I am currently asking myself the following question:
Is it recommended that I define my state and logic directly in the ContextProvider or is it okay if I define the state and logic in a separate function to separate the code a bit?
Example:
const MyContext = React.createContext({});
const createStore = () => {
const [myState, setMyState] = useState();
return {
myState,
setMyState
}
}
const MyContextProvider = ({ children }) => {
const store = createStore();
return (
<MyContext.Provider value={store}>{children}</MyContext.Provider>
)
}
I am a little bit affraid of that createStore function. Does the createStore always gets recreated if the Provider rerenders ?
Edit:
Thanks for the answer!
What if I want to use a parameter in the useCreateStore hook ?
Will the parameter gets updated?
Example:
const MyContext = React.createContext({});
const useCustomStore= (myAwesomeValue) => {
const [myState, setMyState] = useState();
const doSomething = useCallback(() => {
//
}, [myAwesomeValue])
return {
myState,
setMyState
}
}
const MyContextProvider = ({ children, title }) => {
const { myState } = useCustomStore(title); //You need to desctructure the returned object here, note myState
return (
<MyContext.Provider value={myState}>{children}</MyContext.Provider>
)
}
What you are trying to create for your "store" is called a custom hook
You will need to make some changes though. It is customary to use 'use' as the start of a custom hook. so, here I have renamed createStore to useCustomStore. Since it is a custom hook with useState, it follows the same rules as if you actually had it within your context provider
Also, your custom hook returns an object which contains the state and a mutation method. you will need to access the state either directly store.myState or you can destructure it { myState} as I have in the example.
const MyContext = React.createContext({});
const useCustomStore= () => {
const [myState, setMyState] = useState();
return {
myState,
setMyState
}
}
const MyContextProvider = ({ children }) => {
const { myState } = useCustomStore(); //You need to desctructure the returned object here, note myState
return (
<MyContext.Provider value={myState}>{children}</MyContext.Provider>
)
}
Is the same as
const MyContext = React.createContext({});
const MyContextProvider = ({ children }) => {
const [myState, setMyState] = useState();
return (
<MyContext.Provider value={myState}>{children}</MyContext.Provider>
)
}
So rerenders will preserve state, since it uses the useState hook.
Related
class Dashboard extends Component {
constructor(props) {
super(props)
this.state = {
assetList: [],
assetList1: [];
}
}
componentDidMount = async () => {
const web3 = window.web3
const LandData=Land.networks[networkId]
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address)
this.setState({ landList })
}
}
...
}
In this code the state for landlist is not defines in constructor but setState is used. If I have to convert the code to a function component, what will be the equivalent code?
In React class components, there existed a single state object and you could update it with any properties you needed. State in React function components functions a little differently.
React function components use the useState hook to explicitly declare a state variable and updater function.
You can use a single state, and in this case the functionality would be pretty similar, keeping in mind though that unlike the this.setState of class components, the useState
Example:
const Dashboard = () => {
const [state, setState] = React.useState({
assetList: [],
assetList1: []
});
useEffect(() => {
const web3 = window.web3;
const LandData = Land.networks[networkId];
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address);
setState(prevState => ({
...prevState,
landList,
}));
}
}, []);
return (
...
);
};
With the useState hook, however, you aren't limited to a single state object, you can declare as many state variables necessary for your code to function properly. In fact it is recommended to split your state out into the discrete chunks of related state.
Example:
const Dashboard = () => {
const [assetLists, setAssetLists] = React.useState({
assetList: [],
assetList1: []
});
const [landList, setLandList] = React.useState([]);
useEffect(() => {
const web3 = window.web3;
const LandData = Land.networks[networkId];
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address);
setLandList(landList);
}
}, []);
return (
...
);
};
const Dashboard = () => {
const [assetList, setAssetList] = useState([])
const [assetList1, setAssetList1] = useState([])
useEffect(() => {
const web3 = window.web3
const LandData = Land.networks[networkId]
if (LandData) {
const landList = new web3.eth.Contract(Land.abi, LandData.address)
setAssetList(landList)
}
}, [])
I have two hooks, useA and useB, which perform expensive operations (let's assume network API calls) and these values need to be saved into a global application state (AppContext) using React's Context API. Furthermore, the behaviour of the second hook, useB, depends on the result of the first hook, useA.
These hooks are invoked once the application is started and the context provider is created. I call the hooks in the context provider component, and I consume the same context inside the second hook. This creates a problem which results into stale information; the useContext(AppContext) inside useB does not provide correct results for useA. Because the result of useA is needed outside of the context of the useB hook, I cannot move its invocation under the useB hook. Otherwise, this would result into two calls to useA, once inside the context provider and once inside useB.
How could this issue be solved?
const useA = () => {
const api = useApi()
const [state, setState] = useState(null)
useEffect(() => {
api.someFn().then((result) => {
setState(result)
})
}, [api])
return state
}
const useB = () => {
const api = useApi()
const { a } = useContext(AppContext)
const [state, setState] = useState(null)
useEffect(() => {
api.someOtherFn(a.someVariable).then((result) => {
setState(result)
})
}, [api, a])
return state
}
const AppContext = createContext({ a: null, b: null })
const AppContextProvider = (children) => {
const a = useA()
const b = useB()
const context = { a, b }
return <AppContext.Provider value={context}>{children}</AppContext.Provider>
}
const App = () => {
return (
<AppContextProvider>
<Router>
...
</Router>
</AppContextProvider>
)
}
Maybe the following will be useful:
const useA = () => {
const api = useApi()
const [state, setState] = useState(null)
useEffect(() => {
api.someFn().then((result) => {
setState(result)
})
}, [api])
return state
}
const useB = (a) => {
const api = useApi()
const [state, setState] = useState(null)
useEffect(() => {
if (a && a.someVariable) {
api.someOtherFn(a.someVariable).then((result) => {
setState(result)
})
}
}, [api, a])
return state
}
const AppContext = createContext({ a: null, b: null })
const AppContextProvider = (children) => {
const a = useA()
const b = useB(a)
// It is not recommended to pack several values into some object and share it via context
// Cause consumers will be retriggered on each call to AppContextProvider
const context = { a, b }
return <AppContext.Provider value={context}>{children}</AppContext.Provider>
}
const App = () => {
return (
<AppContextProvider>
<Router>
...
</Router>
</AppContextProvider>
)
}
Please read about multiple contexts https://reactjs.org/docs/context.html#consuming-multiple-contexts
I have a custom hook that reads and parses JSON object from the DOM like so:
export const useConfig = () => {
const [config, setConfig] = useState<Config>();
useEffect(() => {
if (document) {
const config = document.getElementById('some-dom-element');
setConfig(JSON.parse(config?.innerHTML || '{}'));
}
}, []);
return config;
};
This object has some meaningful information for different concerns in my application.
So I created another hook that just gets the objects array from the config object:
export const useOptions = () => {
const [options, setOptions] = useState<Options>();
const config = useConfig();
useLayoutEffect(() => {
setOptions(config?.options);
}, [options]);
return options;
};
This is ok, but if I only want to read from the DOM once then I would need to call my useOptions hook at the top level of my component tree and pass the result down to so that components further down the tree can access it.
Is there way that I can change my useOptions hook so that I can call it only in the function components that need it, and not have the DOM read by useConfig on every call?
EDIT: I could probably use Recoil as a lightweight state management solution, but I was wondering if there's a better way to write these hooks so that I don't need to add additional deps
You are describing a custom hook that acts as a singleton.
Try using reusable library, the implementation is simple so you can use it as a recipe:
const useCounter = createStore(() => {
const [counter, setCounter] = useState(0);
return {
counter,
increment: () => setCounter(prev => prev + 1)
}
});
const Comp1 = () => {
const something = useCounter();
}
const Comp2 = () => {
const something = useCounter(); // same something
}
const App = () => (
<ReusableProvider> {/* no initialization code, stores automatically plug into the top provider */}
...
</ReusableProvider>
)
So for your use case, useConfig probably should be in the global store:
const useConfig = {...};
export default createStore(useConfig);
// Always the same config
const config = useConfig();
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>
);
};
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 :)