React new HOOKS api, useReducer with dynamic initialState - reactjs

In case we want to fetch data from remote server and then pass it as initial state to a reducer, how we can proceed ? I tried to call dispatch inside useEffect but it was rejected as per rule of calling hooks inside useEffect(...) is forbidden,
Any help ?

The only way you can pass the data as the initial state to a reducer is to fetch the data before rendering your component. You can do that in the parent:
const Parent = () => {
const [data, setData] = useState(null)
useEffect(() => {
apiCall().then(response => {
setData(response.data)
})
}, [])
if (!data) return null
return <MyComponent data={data} />
}
const MyComponent = ({ data }) => {
const [state, dispatch] = useReducer(reducer, data)
... // utilize state
}
.
The above approach is undesirable as it breaks separation of concerns. Usually it is better to set a null initial state and call dispatch to change it to the desired state:
const MyComponent () => {
const [state, dispatch] = useReducer(reducer, null)
useEffect(() => {
apiCall().then(response => {
dispatch({
type: 'INITIALIZE',
data: response.data
})
})
}, [])
if (!state) return null
... // utilize state
}
.

Related

useReducer - Trying to write own useReducer which will not use dispatch if component is unmounted

could you provide your feedback on the code below:
export function useUnmountSafeReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>] {
const [mounted, setMounted] = useState(true);
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
return () => {
setMounted(false);
};
}, []);
return [state, mounted ? dispatch : () => {}];
}
I am trying to write own reducer which will not use dispatch if component is unmounted.
Try with a ref instead of a state.
const mounted = useRef(true)
useEffect(() => {
return () => {
mounted.current = false
}
}, [])
The reason is that using setMounted is a memory leak used in the destroy function of useEffect. Keep in mind if the component is unmounted, you are not supposed to use any internal method after that. Actually avoiding the memory leak is your reason to implement this mounted at the first place, isn't it?
disabled dispatch
Now the question is can you return a new dispatch after the unmount?
return [state, mounted ? dispatch : () => {}]
After the unmount, there probably won't be any more update to the UI . So the way to get it working is to disable the existing dispatch but not providing an empty one.
const _dispatch = useCallback((v) => {
if (!mounted || !mounted.current) return
dispatch(v)
}, [])
return [state, _dispatch]
The useCallback there might be optional.

How to wait for React useEffect hook finish before moving to next screen

I'm having issue where hook is being used in multiple files and it is being called twice for useEffect before the 1st one's async method finish (which should block the 2nd hook call, but it's not). See below 2 scenarios:
Stack Navigator
const { context, state } = useLobby(); // Hook is called here 1st, which will do the initial render and checks
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)
Lobby Hooks
export const useLobby = () => {
const [state, dispatch] = React.useReducer(...)
//
// Scenario 1
// This get called twice (adds user to room twice)
//
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
}
Queue Screen
const { context, state } = useLobby(); // Hook is called here 2nd right after checking state from stack navigator
//
// Scenario 2
// Only does it once, however after state is changed to active
// the stack navigator didn't get re-render like it did in Scenario 1
//
React.useEffect(() => {
roomLobby.join();
}, []);
return (
...
{state.isActive
? "Show the room Id"
: "Check again"
...
)
In scenario 1, I guess while 1st hook is called and useEffect is doing async to add user to the room and set active to true. Meanwhile the conditional render part is moving straight to Queue screen which calls the hook again and doing the useEffect (since 1st haven't finished and isActive is still false).
How can I properly setup useReducer and useMemo so that it renders the screen base on the state.
Edited codes based on the answer
/* LobbyProvider */
const LobbyContext = React.createContext();
const lobbyReducer = (state, action) => {
switch (action.type) {
case 'SET_LOBBY':
return {
...state,
isActive: action.active,
lobby: action.lobby
};
case 'SET_ROOM':
return {
...state,
isQueued: action.queue,
roomId: action.roomId,
};
default:
return state;
}
}
const LobbyProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(lobbyReducer, initialState);
React.useEffect(() => {
console.log("Provider:", state)
if (!state.isActive) joinRoom();
}, [])
// Using Firebase functions
const joinRoom = async () => {
try {
const response = await functions().httpsCallable('getActiveLobby')();
if (response) {
dispatch({ type: 'SET_LOBBY', active: true, lobby: response.data })
const room = await functions().httpsCallable('assignRoom')({ id: response.data.id });
dispatch({ type: 'SET_ROOM', queue: false, roomId: room.data.id })
}
} catch (e) {
console.error(e);
}
}
return (
<LobbyContext.Provider value={{state, dispatch}}>
{ children }
</LobbyContext.Provider>
)
}
/* StackNavigator */
const {state} = React.useContext(LobbyContext);
return (
<LobbyProvider>
// same as above <LobbyStack.Navigator>
// state doesn't seem to be updated here or to re-render
</LobbyProvider>
);
/* Queue Screen */
const {state} = React.useContext(LobbyContext);
// accessing state.isActive to do some conditional rendering
// which roomId does get rendered after dispatch
You must note that a custom hook will create a new instance of state everytime its called.
For example, you call the hook in StackNavigator component and then again in QueueScreen, so 2 different useReducers will be invoked instead of them sharing the states.
You should instead use useReducer in StackNavigator's parent and then utilize that as context within useLobby hook
const LobbyStateContext = React.createContext();
const Component = ({children}) => {
const [state, dispatch] = React.useReducer(...)
return (
<LobbyStateContext.Provider value={[state, dispatch]]>
{children}
</LobbyStateContext>
)
}
and use it like
<Component>
<StackNavigator />
</Component>
useLobby will then look like
export const useLobby = () => {
const [state, dispatch] = React.useContext(LobbyStateContext)
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
return { context, assignRoom, state};
}
StackNavigator will utilize useLobby and have the useEFfect logic
const { context, state, assignRoom } = useLobby();
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)

How can i use useReducer to assign initial state after calling custom Datafetch hook? I keep getting null

I created a custom datafetch hook but when i use the reducer function to set it as initial state it says its null.
Component where i call the custom Hook.
const collection = 'items'
const whereClause = { array: "lists", compare: 'array-contains', value: 'Pantry' }
const res = useDataFetchWhere(collection, whereClause)
const data = res.response
const [state, dispatch] = useReducer(reducer, data)
When I console.log(state) I get null.
My custom data fetch hook
const useDataFetchWhere = (collection, whereClause) => {
const [response, setResponse] = useState(null)
const [error, setError] = useState(null)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
setError(false)
try {
await db.collection(collection).where(whereClause.array, whereClause.compare, whereClause.value).get()
.then(data => {
setResponse(data.docs.map(doc => ({ ...doc.data(), id: doc.id })))
setIsLoading(false)
console.log('hello where')
})
} catch (error) {
setError(error)
}
}
fetchData()
return function cleanup() {
console.log('cleaned up check')
};
}, [])
return { response, error, isLoading }
}
Is there anything i need to do or call in a different way?
Thanks.
The problem is that useDataFetchWhere does not immediately return the result of the data fetching, but only after a while the request is done and then the setResponse will set the actual data. So you cannot set the response as initial state for the useReducer call.
You need to wait until the request is done before using it's result. You could create an action (e.g. SET_DATA) for the reducer that sets the result once it's there.
You already have the isLoading flag available:
const [state, dispatch] = useReducer(reducer, null);
useEffect(() => {
if (!isLoading) {
const data = res.response;
dispatch({type: 'SET_DATA', data});
}
}, [isLoading]);

React - Maximum update depth exceeded

I am using useAxios hook to fetch data from Api then use add data to global state by Constate library.
However, I am getting error: "Maximum update depth exceeded ". So, how to make it work ? Also, what is best practice to handle asynchronous Api and global state in my use case ?
In index Component:
const {loading, error} = useGetFavoriteTracks()
const {state} = useTrackContext()
In useGetFavoriteTracks.js, fetch data then add favorite tracks to global state
export const useGetFavoriteTracks = params => {
const { data, error, loading } = useAxios({
axiosInstance: myApiAxiosInstance,
url: myApiEndPoint.FAVORITE_TRACKS
});
const { addFavoriteTracks } = useTrackContext();
addFavoriteTracks(data); //add favorite tracks to global state
return { loading, error };
};
TrackContext.js
const useTrack = params => {
const [state, dispatch] = useReducer(reducer, initialState);
const addFavoriteTracks = (data) => {
dispatch({
type: trackActionTypes.FAVORITE_TRACKS_FETCHED_SUCCESS,
payload: data
})
}
return {state, addFavoriteTracks}
}
export const [TrackProvider, useTrackContext] = constate(useTrack);
Rule of thumb: any direct or indirect call to setState or dispatch must not be fired in the process of rendering.
In useGetFavoriteTracks you must not unconditionally call addFavoriteTracks(data); like that. Put that call inside useEffect.
useEffect(() => {
if (!loading && data) addFavoriteTracks(data);
}, [loading, data])

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