React Hooks -Redux Saga Middleware - reactjs

I am creating a React application with Saga middleware. And I have few doubts to clarify.
On the first mount, why is it we are destructing an unknown prop const { onGetPhonesAndTablets } = props which is not passed from the Redux Store (State Container).
Why is it required to setPhoneAndTabletsList(phonesAndTablets) before dispatching an action onGetPhonesAndTablets . As the prop phonesAndTablets will have its value after dispatching an action i.e., onGetPhonesAndTablets
const Home = props => {
const {
phonesAndTablets
} = props
const [phonesAndTabletsList, setPhoneAndTabletsList] = useState([]);
useEffect(() => {
const {
onGetPhonesAndTablets
} = props
setPhoneAndTabletsList(phonesAndTablets)
onGetPhonesAndTablets()
}, [])
useEffect(() => {
if (!isEmpty(phonesAndTablets)) setPhoneAndTabletsList(phonesAndTablets)
}, [phonesAndTablets])
return (
<React.Fragment>
<ProductCategoryList list={phonesAndTabletsList}/>
</React.Fragment>
)
}
Home.propTypes = {
phonesAndTablets: PropTypes.array,
onGetPhonesAndTablets : PropTypes.func
}
const mapStateToProps = ({ home }) => ({
phonesAndTablets: home.phonesAndTablets
})
const mapDispatchToProps = dispatch => ({
onGetPhonesAndTablets: () => dispatch(getPhoneAndTablets())
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)

I'm not entirely sure why this app uses a local state to store the redux state, which is passed as a prop. Seems redundant, not to mention bad for performance.
There is a selector hook, which lets components access individual pieces of redux state. useSelector(state => state.home.phonesAndTablets).
You can also pass a second argument, shallowEqual, which makes the
comparison function do a value comparison, as opposed to reference
comparison. This avoids unnecessary re-renders due to objects/arrays
whose contents are equal, but whose references point to different
objects. Remember that ['a'] !== ['a'] in JS.
There is also a dispatch hook to do the same for actions. useDispatch(). Then you can do
const list = useSelector(state => state.home.phonesAndTablets, shallowEqual)
const dispatch = useDispatch()
useEffect(() => {
dispatch(getPhoneAndTablets())
}, [])
return (
<>
<ProductCategoryList list={list ?? []} />
</>
)
I would personally add a check if the list is empty before fetching a new list, but maybe the app is supposed to fetch a new list whenever Home is mounted. I have no idea.

Since, the above code seems redundant as the state is already managed and handled by the State Container (Redux Store). So I tried removing redundant codes.
Personally I prefer mapStateToProps to selectors.
const Home = props => {
const {
phonesAndTablets,
onGetPhonesAndTablets
} = props
useEffect(() => {
onGetPhonesAndTablets()
}, [])
return (
<React.Fragment>
<ProductCategoryList list={phonesAndTablets ? phonesAndTablets : ''}/>
</React.Fragment>
)
}
Home.propTypes = {
phonesAndTablets: PropTypes.array,
onGetPhonesAndTablets : PropTypes.func
}
const mapStateToProps = ({ home }) => ({
phonesAndTablets: home.phonesAndTablets
})
const mapDispatchToProps = dispatch => ({
onGetPhonesAndTablets: () => dispatch(getPhoneAndTablets())
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)

Related

How to update React UI from with state in Redux store with useEffect

I have the following functional component. I did debugging and read some posts about react-redux and useEffect, but still have had no success. On initial render the state in my redux store is null but then changes to reflect new state with data. However, my react UI does not reflect this. I understand what the issue is, but I don't know exactly how to fix it. I could be doing things the wrong way as far as getting the data from my updated state in the redux store.
Here is my component :
const Games = (props) => {
const [gamesData, setGamesData] = useState(null)
const [gameData, setGameData] = useState(null)
const [gameDate, setGameDate] = useState(new Date(2020, 2, 10))
const classes = GamesStyles()
// infinite render if placed in
// useEffect array
const {gamesProp} = props
useEffect(() => {
function requestGames() {
var date = parseDate(gameDate)
try {
props.getGames(`${date}`)
// prints null, even though state has changed
console.log(props.gamesProp)
setGamesData(props.gamesProp)
} catch (error) {
console.log(error)
}
}
requestGames()
}, [gameDate])
// data has not been loaded yet
if (gamesData == null) {
return (
<div>
<Spinner />
</div>
)
} else {
console.log(gamesData)
return (
<div><p>Data has been loaded<p><div>
{/* this is where i would change gameDate */}
)
}
}
const mapStateToProps = (state) => {
return {
gamesProp: state.gamesReducer.games,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getGames: (url) => dispatch(actions.getGames(url)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Games)
Here is my reducer
import {GET_GAMES} from '../actions/types'
const initialState = {
games: null // what we're fetching from backend
}
export default function(state = initialState, action){
switch(action.type){
case GET_GAMES:
// prints correct data from state
//console.log(action.payload)
return{
...state,
games: action.payload
}
default:
return state
}
}
Here is my action
import axios from 'axios'
import {GET_GAMES} from './types'
// all our request go here
// GET GAMES
export const getGames = (date) => dispatch => {
//console.log('Date', date)
axios.get(`http://127.0.0.1:8000/games/${date}`)
.then(res => {
dispatch({
type: GET_GAMES,
payload: res.data
})
}).catch(err => console.log(err))
}
When I place the props from state in my dependencies array for useEffect, the state updates but results in an infinite render because the props are changing.
This happens even if I destruct props.
Here is an image of my redux state after it is updated on the initial render.
You were running into issues because you were trying to set the state based off of data that was in a closure. The props.gamesProp within the useEffect you had would never update even when the parent data changed.
The reason why props.gamesProp was null in the effect is because in each render, your component essentially has a new instance of props, so when the useEffect runs, the version of props that the inner part of the useEffect sees is whatever existed at that render.
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function
Unless you have to modify gamesState within your component, I highly recommend that you don't duplicate the prop to the state.
I'd also recommend using useDispatch and useSelector instead of connect for function components.
Here's some modifications to your component based on what I see in it currently and what I've just described:
import { useDispatch, useSelector } from 'react-redux';
const Games = (props) => {
const gamesData = useSelector((state) => state.gamesReducer.games);
const dispatch = useDispatch();
const [gameData, setGameData] = useState(null);
const [gameDate, setGameDate] = useState(new Date(2020, 2, 10));
const classes = GamesStyles();
// infinite render if placed in
// useEffect array
useEffect(() => {
const date = parseDate(gameDate);
try {
dispatch(actions.getGames(`${date}`));
} catch (error) {
console.log(error);
}
}, [gameDate, dispatch]);
// data has not been loaded yet
if (gamesData == null) {
return (
<div>
<Spinner />
</div>
);
} else {
console.log(gamesData);
return (
<div>
<p>Data has been loaded</p>
</div>
// this is where i would change gameDate
);
}
};
export default Games;
If you need to derive your state from your props, here's what the React Documentation on hooks has to say:
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops

React context state value not updated in Consumer

The first value set to "search term" through the "dispatcher" persists after any subsequent calls and I'm trying to figure out why that is or where the error is.
I've got a <ContextProvider /> where a state for "search term" is defined, and the value for the "search term" might change by an event that is triggered by the <ContextConsumer />, or nested <ContextConsumer /> component by a "dispatcher". I'm finding that the desired state is not found, after the call to the "reducer", even considering that the "state" change is not immediately.
For brevity, the Components or the code posted below was simplified to isolate the subject, so there might be a few typos like not declared variables (as I've removed chunks of code that is not related).
The Context Provider looks like:
import React from 'react'
export const POSTS_SEARCH_RESULTS = 'POSTS_SEARCH_RESULTS'
export const GlobalStateContext = React.createContext()
export const GlobalDispatchContext = React.createContext()
const initialState = {
posts: [],
searchTerm: ''
}
const reducer = (state, action) => {
switch (action.type) {
case POSTS_SEARCH_RESULTS: {
return {
...state,
posts: action.posts,
searchTerm: action.searchTerm
}
}
default:
throw new Error('Bad Action Type')
}
}
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState)
return (
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>
{children}
</GlobalDispatchContext.Provider>
</GlobalStateContext.Provider>
)
}
export default GlobalContextProvider
The Consumer looks like:
const Search = () => {
const state = useContext(GlobalStateContext)
const { searchTerm, posts } = state
useEffect(() => {
console.log('[debug] <Search />: searchTerm: ', searchTerm);
}, [searchTerm])
return (
<>
<LoadMoreScroll searchTerm={searchTerm} posts={posts} postCursor={postCursor} />
</>
)
}
export default Search
Following up is the nested Consumer Children Component. The useEffect has a dependency for searchTerm; This value is set through the "dispatcher" and get through the useContenxt in a Consumer.
dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm: term })
And consumed like so:
const state = useContext(GlobalStateContext)
const { searchTerm, posts } = state
And passed to, for example <LoadMoreScroll searchTerm={searchTerm} />
So, what I have and it fails is:
const LoadMoreScroll = ({ searchTerm, posts, postCursor }) => {
const dispatch = useContext(GlobalDispatchContext)
const [postsCached, setPostsCached] = useState(posts)
const [loading, setLoading] = useState(false)
const refScroll = useRef(null)
const [first] = useState(POSTS_SEARCH_INITIAL_NUMBER)
const [after, setAfter] = useState(postCursor)
const [isVisible, setIsVisible] = useState(false)
const [term, setTerm] = useState(searchTerm)
useEffect(() => {
loadMore({ first, after, term })
}, [isVisible])
useEffect(() => {
dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm })
}, [postsCached])
useEffect(() => {
setTerm(searchTerm)
const handler = _debounce(handleScroll, 1200)
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [searchTerm])
const handleScroll = () => {
const offset = -(window.innerHeight * 0.1)
const top = refScroll.current.getBoundingClientRect().top
const isVisible = (top + offset) >= 0 && (top - offset) <= window.innerHeight
isVisible && setIsVisible(true)
}
const loadMore = async ({ first, after, term }) => {
if (loading) return
setLoading(true)
const result = await searchFor({
first,
after,
term
})
const nextPosts = result.data
setPostsCached([...postsCached, ...nextPosts])
setAfter(postCursor)
setLoading(false)
setIsVisible(false)
}
return (
<div ref={refScroll} className={style.loaderContainer}>
{ loading && <Loader /> }
</div>
)
}
export default LoadMoreScroll
The expected result is to have <LoadMoreScroll />'s to pass to the "loadMore" function the latest value of "searchTerm" assigned by the "dispatcher", which fails. What it does instead is that it consumes the "initial value" from a first call to the "dispatcher". This is after the initial call to the "dispatcher" any subsequent "dispatcher" call:
dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm: term })
That should update the Context "searchTerm", fails to do. In the source code above, the loadmore holds the initial value that was set!
Separate example the has a similar logic, works without any issues ( https://codesandbox.io/s/trusting-booth-1w40e?fontsize=14&hidenavigation=1&theme=dark )
Hope to update the issue above with a solution soon, in case somebody spots the issue, please let me know!
The codesandbox link works, but doesn't seem to be using the same pattern as the code above when it comes to creating and using context.
In the provided code you have created two separate providers. One has a value of state and one has a value of dispatch.
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>
The codesandbox however is using both state and dispatch within the same provider.
<Application.Provider value={{ state, dispatch }}>
Also it seems that GlobalContextProvider is exported, but I'm not sure if it is used to wrap any consumers.
Since there is a separation of dispatch and state, I am going to use this for my proposed solution.
The implementation seems correct, but in my opinion you could take this a step further and create two custom hooks, that expose only one way to provide the context value and only one way to consume it.
import React from "react";
export const POSTS_SEARCH_RESULTS = "POSTS_SEARCH_RESULTS";
//
// notice that we don't need to export these anymore as we are going to be
//
// using them in our custom hooks useGlobalState and useGlobalDispatch
//
//
const GlobalStateContext = React.createContext();
const GlobalDispatchContext = React.createContext();
const initialState = {
posts: [],
searchTerm: "",
};
const reducer = (state, action) => {
switch (action.type) {
case POSTS_SEARCH_RESULTS: {
return {
...state,
posts: action.posts,
searchTerm: action.searchTerm
};
}
default:
throw new Error("Bad Action Type");
}
};
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>
{children}
</GlobalDispatchContext.Provider>
</GlobalStateContext.Provider>
);
};
// If any of these hooks is not being called within a function component
// that is rendered within the `GlobalContextProvider`,
// we throw an error
const useGlobalState = () => {
const context = React.useContext(GlobalStateContext);
if (context === undefined) {
throw new Error(
"useGlobalState must be used within a GlobalContextProvider"
);
}
return context;
};
const useGlobalDispatch = () => {
const context = React.useContext(GlobalDispatchContext);
if (context === undefined) {
throw new Error(
"useGlobalDispatch must be used within a GlobalContextProvider"
);
}
return context;
};
// We only export the custom hooks for state and dispatch
// and of course our`GlobalContextProvider`, which we are
// going to wrap any part of our app that
// needs to make use of this state
export { GlobalContextProvider, useGlobalState, useGlobalDispatch };
All I've added here is a couple of custom hooks that expose each of the contexts, i.e GlobalStateContext and GlobalDispatchContext and export them along with the GlobalContextProvider.
If we wanted to make this globally available throughout the app, we could wrap the GlobalContextProvider around the App component.
function App() {
return (
<div className="App">
<Search />
</div>
);
}
// If you forget to wrap the consumer with your provider, the custom hook will
// throw an error letting you know that the hook is not being called
// within a function component that is rendered within the
// GlobalContextProvider as it's supposed to
const AppContainer = () => (
<GlobalContextProvider>
<App />
</GlobalContextProvider>
);
export default AppContainer;
If you want to either use the state in any part of your app, or dispatch any action, you will need to import the relevant custom hook created earlier.
In your Search component this would look like the example below:
import { useGlobalState, useGlobalDispatch } from "./Store";
const Search = () => {
// Since we are doing this in our custom hook that is not needed anymore
// const state = useContext(GlobalStateContext)
// if you need to dispatch any actions you can
// import the useGlobalDispatch hook and use it like so:
// const dispatch = useGlobalDispatch();
const state = useGlobalState();
const { searchTerm, posts } = state
useEffect(() => {
console.log('[debug] <Search />: searchTerm: ', searchTerm);
}, [searchTerm])
return (
<>
<LoadMoreScroll searchTerm={searchTerm} posts={posts} postCursor={postCursor} />
</>
)
}
export default Search
Since there were a few parts missing in the codesandbox provided in the question, I've refactored it to a simplified working version of this concept here that hopefully will help solve your issue.
I've also found this article quite helpful when I had problems with Context API and hooks.
It is following that same pattern, I've been using this in production and have been quite happy with the results.
Hope that helps :)

React Hooks & React-Redux connect: What is the best practice to make them work together?

I would like to ask to the community what is the best practice to combine existing code using react-redux connect() and dependency management in hooks like useEffect().
Let's look at the following example:
/* ... */
const mapStateToProps = (state) => ({
todos: getTodos(state),
currentUserId: getCurrentUserId(state)
})
const mapDispatchToProps = (dispatch) => ({dispatch})
const mapMergeProps = (stateProps, {dispatch}, ownProps) => {
return {
...stateProps,
...dispatchProps,
...ownProps,
fetchTodos: () => dispatch(fetchTodos(stateProps.currentUserId))
}
}
const TodosListContainer = connect(mapStateToProps, mapDispatchToProps, mapMergeProps)
const TodosList = ({todos, fetchTodos, ...props}) => {
const _fetchTodos = useCallback(fetchTodos, [])
useEffect(() => {
_fetchTodos()
}, [_fetchTodos])
return (
<ul>
{todos && todos.map((todo) => <li key={todo.id}>{todo.name}</li>)}
</ul>
)
}
In the code above useEffect has all the dependencies, and useCallback ensures that useEffect is triggered only once.
It seems however like an extra layer of boilerplate: Without useCallback, the functions passed down as props that are coming from connect's mapMergeToProps will be recreated on every change of state, and will trigger useEffect.
My question is whether the code above is correct, and there's a better way to handle useEffect in the context described.
I would change it to the following
// Leave this one like it is
const mapStateToProps = (state) => ({
todos: getTodos(state),
currentUserId: getCurrentUserId(state)
})
// Map the fetchTodos here
const mapDispatchToProps = {
fetchTodos,
}
// Completely remove the mergeProps
const TodosListContainer = connect(mapStateToProps, mapDispatchToProps)
// Then use the hook like this
const TodosList = ({ todos, fetchTodos, currentUserId, ...props }) => {
useEffect(() => {
fetchTodos(currentUserId)
}, [fetchTodos, currentUserId])
return (
<ul>
{todos && todos.map((todo) => <li key={todo.id}>{todo.name}</li>)}
</ul>
)
}
This will not trigger useEffect on each render because the props stay the same. It removes (some of the) boilerplate code. I also think it makes it easier to understand what is going and it will run the effect when the currentUserId changes, which is probably what is meant to happen.

Retrieve current state from useReducer outside React component

I am leveraging the useReducer hook with Context to create a Redux-ish state store supporting middleware.
const Provider = (props: any) => {
const [state, dispatch] = React.useReducer(reducer, {
title: 'Default title',
count: 0,
});
const actionDispatcher = makeActionDispatcher(
dispatch,
applyMiddleware(state, thunkMiddleware, callApiMiddleware, logger),
);
return (
<Context.Provider value={{ ...state, ...actionDispatcher }}>
{props.children}
</Context.Provider>
);
};
Note that I am passing state to applyMiddleware:
const applyMiddleware = (state: {}, ...middlewares: Function[]) =>
function dispatcher(dispatch: Function) {
const middlewareAPI = {
state,
dispatch: (...args) => dispatch(...args),
};
const chain = middlewares.map((middleware) => {
return middleware(middlewareAPI);
});
return compose(...chain)(dispatch);
};
This works, but eventually I want to be able to work with async actions, so ideally I'd have something like redux-thunk:
function thunkMiddleware(store: Store) {
return (next: Function) => (action: any) => {
typeof action === 'function' ? action(next, store.getState) : next(action);
};
}
Given the thunk middleware will be acting upon async actions, ideally we would be able to pass a function to retrieve the current state when needed - getState - rather than be forced to use state as it existed when the middleware was applied, which could be out of date.
Normally I would pass something like this down:
const getState = () => React.useReducer(reducer, {
title: 'Default title',
count: 0,
})[0];
But if I pass that down to middleware to be invoked, I get an error indicating I can only call hooks from React functions.
Am I architecting things wrong? Am I not properly wrapping my head around hooks?
UPDATE: adding requested makeActionDispatcher implementation
export const makeActionDispatcher = (
dispatch: React.Dispatch<any> | undefined,
enhancer?: Function,
): ActionDispatcher => {
const actionDispatcher: { [key: string]: (...args: any) => void } = {};
Object.keys(actionCreators).forEach((key) => {
const creator = actionCreators[key];
actionDispatcher[key] = (...args: any) => {
if (!dispatch) {
throw new Error('ActionDispatcher has not been initialized!');
}
const action = creator(...args);
if (enhancer) {
const enhancedDispatch = enhancer(dispatch);
enhancedDispatch(action);
} else {
dispatch(action);
}
};
});
return actionDispatcher as ActionDispatcher;
};
Use the useEnhancedReducer hook introduced here.
Then you will have something like.
const [state, dispatch, getState] = useEnahancedReducer(reducer, initState)
Because dispatch, getState will never change, you can pass it to some hook without adding them to the dependence list or store them somewhere else to call them from outside.
There is also version of useEnhancedReducer which supports adding middleware, in the same post.

How to manipulate context - attach function to context or wrap dispatch in hook?

I'm wondering what the recommended best practice is for manipulating and exposing the new React Context.
The easiest way to manipulate context state seems to be to just attach a function to the context that either dispatches (usereducer) or setstate (useState) to change its internal value once called.
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
return (
<Context.Provider
value={{
todos: state.todos,
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
}}
>
{children}
</Context.Provider>
);
};
export const Todos = id => {
const { todos, fetchTodos } = useContext(Context);
useEffect(() => {
if (fetchTodos) fetchTodos(id);
}, [fetchTodos]);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
I was however told exposing and using the react context object directly is probably not a good idea, and was told to wrap it inside a hook instead.
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
return (
<Context.Provider
value={{
dispatch,
state
}}
>
{children}
</Context.Provider>
);
};
const useTodos = () => {
const { state, dispatch } = useContext(Context);
const [actionCreators, setActionCreators] = useState(null);
useEffect(() => {
setActionCreators({
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
});
}, []);
return {
...state,
...actionCreators
};
};
export const Todos = ({ id }) => {
const { todos, fetchTodos } = useTodos();
useEffect(() => {
if (fetchTodos && id) fetchTodos(id);
}, [fetchTodos]);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
I have made running code examples for both variants here: https://codesandbox.io/s/mzxrjz0v78?fontsize=14
So now I'm a little confused as to which of the 2 ways is the right way to do it?
There is absolute no problem with using useContext directly in a component. It however forces the component which has to use the context value to know what context to use.
If you have multiple components in the App where you want to make use of TodoProvider context or you have multiple Contexts within your app , you simplify it a little with a custom hook
Also one more thing that you must consider when using context is that you shouldn't be creating a new object on each render otherwise all components that are using context will re-render even though nothing would have changed. To do that you can make use of useMemo hook
const Context = React.createContext<{ todos: any; fetchTodos: any }>(undefined);
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
const context = useMemo(() => {
return {
todos: state.todos,
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
};
}, [state.todos, getTodos]);
return <Context.Provider value={context}>{children}</Context.Provider>;
};
const getTodos = async id => {
console.log(id);
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/" + id
);
return await response.json();
};
export const useTodos = () => {
const todoContext = useContext(Context);
return todoContext;
};
export const Todos = ({ id }) => {
const { todos, fetchTodos } = useTodos();
useEffect(() => {
if (fetchTodos) fetchTodos(id);
}, [id]);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
Working demo
EDIT:
Since getTodos is just a function that cannot change, does it make
sense to use that as update argument in useMemo?
It makes sense to pass getTodos to dependency array in useMemo if getTodos method is changing and is called within the functional component. Often you would memoize the method using useCallback so that its not created on every render but only if any of its dependency from enclosing scope changes to update the dependency within its lexical scope. Now in such a case you would need to pass it as a parameter to the dependency array.
However in your case, you can omit it.
Also how would you handle an initial effect. Say if you were to call
`getTodos´ in useEffect hook when provider mounts? Could you memorize
that call as well?
You would simply have an effect within Provider that is called on initial mount
export const TodosProvider: React.FC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, null, init);
const context = useMemo(() => {
return {
todos: state.todos,
fetchTodos: async id => {
const todos = await getTodos(id);
console.log(id);
dispatch({ type: "SET_TODOS", payload: todos });
}
};
}, [state.todos]);
useEffect(() => {
getTodos();
}, [])
return <Context.Provider value={context}>{children}</Context.Provider>;
};
I don't think there's an official answer, so let's try to use some common sense here. I find perfectly fine to use useContext directly, I don't know who told you not to, perhaps HE/SHE should have pointed for official docs. Why would the React team create that hook if it wasn't supposed to be used? :)
I can understand, however, trying to avoid creating a huge object as the value in the Context.Provider, one that mixes state with functions that manipulate it, possibly with async effects like your example.
However, in your refactor, you introduced a very weird and absolutely unnecessary useState for the action creator that you simply had defined inline in your first approach. It seems to me you were looking for useCallback instead. So, why don't you mix both like this?
const useTodos = () => {
const { state, dispatch } = useContext(Context);
const fetchTodos = useCallback(async id => {
const todos = await getTodos(id)
dispatch({ type: 'SAVE_TODOS', payload: todos })
}, [dispatch])
return {
...state,
fetchTodos
};
}
Your calling code doesn't need that weird check to verify that fetchTodos indeed exists.
export const Todos = id => {
const { todos, fetchTodos } = useContext(Context);
useEffect(() => {
fetchTodos()
}, []);
return (
<div>
<pre>{JSON.stringify(todos)}</pre>
</div>
);
};
Finally, unless you actually need to use this todos + fetchTodos combo from more components down the tree from Todos, which you didn't explictly stated in your question, I think using Context is complicating matters when they're not needed. Remove the extra layer of indirection and call useReducer directly in your useTodos.
It may not be the case here, but I find people are mixing a lot of things in their head and turning something simple into something complicated (like Redux = Context + useReducer).
Hope it helps!

Resources