I have several components that require access to the same list of players. This list of players won't change, but I do not know what component will required it first. My idea here is that whoever component requires it first (axios request) updates my redux store and then the other ones would just use this same custom hook for that.
export default function usePlayersList() {
const {list, loading, error} = useSelector(state => state.accounts.players)
const dispatch = useDispatch()
useEffect(() => {
try {
const response = authAxios.get(endpoints.players.retrieve)
response.then(res => {
dispatch({
type: "SET_PLAYERS_LIST",
payload: {
error: false,
loading: false,
list: res.data.payload
}
})
})
} catch (e) {
console.log(e)
dispatch({
type: "SET_PLAYERS_LIST",
payload: {
error: true,
loading: false,
list: []
}
})
}
}, [dispatch])
return {list, loading, error}
}
This is my custom hook for that. Wondering if there is any inconvenience with doing that or even better patterns for such. Thanks in advance.
BTW... Even better, I would set loading and error as useState variables inside my usePlayersList (as opposed to sending them to redux state). Since this lead me to error (loading and error became different on each component, assuming that this state became individual to each component) I sent them to store.
Best regards, Felipe.
TL;DR use Context
Explanation:
Each component gets its instance of the custom hook.
In the example below, we can see that calling setState
changes the state in the Bla
component but not in the Foo
component:
const { Fragment, useState, useEffect } = React;
const useCustomHook = () => {
const [state, setState] = useState('old');
return [state, setState];
};
const Foo = () => {
const [state] = useCustomHook();
return <div>state in Foo component: {state}</div>;
};
const Bla = () => {
const [state, setState] = useCustomHook();
return (
<div>
<div>state in Bla component: {state}</div>
<button onClick={() => setState("new")}>setState</button>
</div>
);
};
function App() {
return (
<Fragment>
<Foo />
<Bla />
</Fragment>
);
}
ReactDOM.render(<App />, document.querySelector('#root'));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>
A combination of Context and custom hook can be used to fix the above issue:
const { createContext, useContext, useState, useEffect } = React;
const Context = createContext();
const ContextProvider = ({ children }) => {
const value = useState("old");
return <Context.Provider value={value} children={children} />;
};
const useCustomHook = () => {
const [state, setState] = useContext(Context);
return [state, setState];
};
const Foo = () => {
const [state] = useCustomHook();
return <div>state in Foo component: {state}</div>;
};
const Bla = () => {
const [state, setState] = useCustomHook();
return (
<div>
<div>state in Bla component: {state}</div>
<button onClick={() => setState("new")}>setState</button>
</div>
);
};
function App() {
return (
<ContextProvider>
<Foo />
<Bla />
</ContextProvider>
);
}
ReactDOM.render(<App />, document.querySelector('#root'));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>
Original answer that's specific to what OP wants:
Defining the context:
import React from 'react';
const PlayersContext = React.createContext();
const PlayersProvider = PlayersContext.Provider;
export const usePlayersContext = () => React.useContext(PlayersContext);
export default function PlayersProvider({ children }) {
// add all the logic, side effects here and pass them to value
const { list, loading, error } = useSelector(
(state) => state.accounts.players
);
const dispatch = useDispatch();
useEffect(() => {
try {
const response = authAxios.get(endpoints.players.retrieve);
response.then((res) => {
dispatch({
type: 'SET_PLAYERS_LIST',
payload: {
error: false,
loading: false,
list: res.data.payload,
},
});
});
} catch (e) {
console.log(e);
dispatch({
type: 'SET_PLAYERS_LIST',
payload: {
error: true,
loading: false,
list: [],
},
});
}
}, [dispatch]);
return <PlayersProvider value={{ list, loading, error }}>{children}</PlayersProvider>;
}
Add the PlayersProvider
as a parent to the components that need access to the players.
and inside those child components:
import {usePlayersContext} from 'contextfilepath'
function Child() {
const { list, loading, error } = usePlayersContext()
return ( ... )
}
You can also maintain the state in the Provider
itself instead of in the redux.