Augmenting dispatch causes unstable identity - reactjs

I'm augmenting Context's dispatch function like this:
const augmentDispatch = (
dispatch: React.Dispatch<Action | ApiAction>,
state: AppState,
) => (action: Thunk | Action | ApiAction) => {
if (typeof action === "object" && action.type === CALL_API) {
return callApi(action as ApiAction, dispatch);
}
return action instanceof Function
? action(dispatch, state)
: dispatch(action as Action);
};
It allows me to trigger an API call for certain actions. I then use it like this:
export function App(): JSX.Element {
const [state, dispatch] = useReducer(rootReducer, defaultState);
const augmentedDispatch = augmentDispatch(dispatch, state);
return (
<GlobalContext.Provider
value={{ state, dispatch: augmentedDispatch }}
>
This works great for event handlers, but I ran into an infinite re-render when I attempted to use it in useEffect:
export const EditForm: React.FC = () => {
const { dispatch, state } = useContext(GlobalContext);
const dispatchLoadDetails = useCallback(
() => dispatch(loadDetails()),
[dispatch],
);
useEffect(() => {
dispatchLoadDetails();
}, [dispatchLoadDetails]);
return (
<Form
I seem to have gotten around it by capturing a reference to the augmented dispatch:
export function App(): JSX.Element {
const [state, dispatch] = useReducer(rootReducer, defaultState);
/**
* Note the usage of useRef here - without it we'd generate a new reference to
* augmentedDispatch with every call within useEffect, which would cause an
* indfinite re-render
*/
const augmentedDispatch = useRef(augmentDispatch(dispatch, state));
return (
<GlobalContext.Provider
value={{ state, dispatch: augmentedDispatch.current }}
>
Is this the best way to solve this problem? The docs indicate useRef should be used as a last resort...

Related

React exporting useContext causes errors

Context.js
const GlobalContext = React.createContext();
const initState = {count:0};
const GlobalContextProvider = props => {
const [state, setState] = useState(initState);
return (
<GlobalContext.Provider value={{state:state, setState:setState}}>
{props.children}
</GlobalContext.Provider>
)
};
const GlobalContextValue = useContext(GlobalContext)
export {GlobalContextValue, GlobalContextProvider}
When I exported the GlobalContextValue, Chrome or React throws an error saying this is an invalid hook call, but I want to be able use setState in a module that's showing below.
fetchAPI.js
import { GlobalContextValue } from './GlobalContext';
const {state, setState} = GlobalContextValue;
function load() {
fetch('localhost:8000/load')
.then(res => res.json())
.then(json => setState(json));
};
You can't use hooks outside of React functional components.
You can probably do this another way though.
Disclaimer: I didn't test this code, but it should do what you want, although I don't recommend doing this at all.
const GlobalContext = React.createContext();
const globalState = { count: 0 }
let subscribers = []
export function setGlobalState(value) {
Object.assign(globalState, value)
subscribers.forEach(f => f(globalState))
}
export function subscribe(handler) {
subscribers.push(handler)
return () => {
subscribers = subscribers.filter(s => s !== handler)
}
}
const GlobalContextProvider = props => {
const [state, setState] = useState(globalState)
useEffect(() => subscribe(setState), [])
return (
<GlobalContext.Provider value={{ state: state, setState: setGlobalState }}>
{props.children}
</GlobalContext.Provider>
);
};

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 :)

ReactJS Context > Reference latest state value from a state function

I'm currently using a common Context pattern I've seen, which allows child components to update a parent's state (i.e. the Provider) by passing a modifier function down through the shared Context.
The problem I'm having, is that the modifier functions only reference the original state, and don't reference the latest state...
Is there a way to get the latest state values (i.e. state.user) from the modifier function (i.e. modUser)?
Below is the minimum code to reproduce:
Expected output: Nick ... then Bob ... then BOB.
Actual output: Nick ... then Bob ... then NICK.
import React, { useState, useContext, useEffect } from "react"
const defaultState = {
user: null,
setUser: () => { },
modUser: () => { }
}
const Context = React.createContext(defaultState)
const Test = () => {
const setUser = user => setState(prevState => ({ ...prevState, user }))
const modUser = () => setUser(state.user.toUpperCase()) // `state.user` never changes to updated value!
// user is 'Nick' at start...
const initState = {
user: 'Nick',
setUser,
modUser
}
// user changes to 'Bob' after 1 second...
useEffect(() => { setTimeout(() => setUser('Bob'), 1000) }, [])
const [state, setState] = useState(initState)
return <Context.Provider value={state}>
<div>Parent user is {state.user}</div>
<TestInner />
</Context.Provider>
}
const TestInner = () => {
const state = useContext(Context)
// user (should) change to uppercase 'BOB' after 2 seconds...
useEffect(() => { setTimeout(() => state.modUser(), 2000) }, [])
return <div>Child user is {state.user}</div>
}
export default Test
You can pass a callback to the state setter. Looking a little closer at your code there is no need to wrap setUser or modUser in a useCallback because after const [state, setState] = useState(initState); you never change them. The following can work and initializes the state once so mod and setUser don't need to be re created on re render of Test (because they are only used on first render and then ignored.
const { useState, useContext, useEffect } = React;
const Context = React.createContext();
const Test = () => {
//use callback to set state with initial value, after
// first render the callback will not be called again
// until component is unmounted and re mounted
const [state, setState] = useState(() => ({
setUser: user =>
setState(prevState => ({ ...prevState, user })),
modUser: () =>
setState(state => ({
...state,
user: state.user.toUpperCase(),
})),
user: 'Nick',
}));
//get the setUser function to pass it to useEffect as a dependency
const { setUser } = state;
useEffect(() => {
setTimeout(() => setUser('Bob'), 1000);
}, [setUser]);
return (
<Context.Provider value={state}>
<div>Parent user is {state.user}</div>
<TestInner />
</Context.Provider>
);
};
const TestInner = () => {
const { modUser, user } = useContext(Context);
useEffect(() => {
setTimeout(() => modUser(), 2000);
}, [modUser]);
return <div>Child user is {user}</div>;
};
ReactDOM.render(<Test />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

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