I am making a rock, paper, scissors game. In the code below, I have a context file that is used to store the global state. I also am showing my choices component. When a user clicks on a button in the choices component, the setChoices method is called which should set the user choice and cpu choice variables in the global state. Then, the cpuScore() method is ran afterwards to increment the cpu score (just to illustrate the problem). The cpu score updates as expected, but the choice variables are not updated. If I do not run the cpuScore method, the choice variables update as expected, but obviously not the score.
//context file
import React, { createContext, useState } from 'react';
const GameContext = createContext();
const GameContextProvider = props => {
const [gameItems, setGameItems] = useState(
{userChoice: null, userScore: 0, cpuChoice: null, cpuScore: 0}
);
const setChoices = (userChoice, cpuChoice) => {
setGameItems({...gameItems, userChoice: userChoice, cpuChoice: cpuChoice})
}
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
return (
<GameContext.Provider value={{gameItems, setChoices, cpuScore}}>
{ props.children }
</GameContext.Provider>
)
}
export default GameContextProvider;
//choices component
import React, { useContext } from 'react';
import { GameContext } from '../contexts/GameContext';
const Choices = (props) => {
const { setChoices, cpuScore } = useContext(GameContext);
const getCpuChoice = () => {
const cpuChoices = ['r', 'p', 's'];
const randomIndex = Math.floor((Math.random() * 3));
const cpuDecision = cpuChoices[randomIndex];
return cpuDecision
}
const playGame = (e) => {
const userChoice = e.target.id;
const cpuChoice = getCpuChoice();
setChoices(userChoice, cpuChoice);
cpuScore();
}
return (
<div>
<h1>Make Your Selection</h1>
<div className="choices">
<i className="choice fas fa-hand-paper fa-10x" id="p" onClick={playGame}></i>
<i className="choice fas fa-hand-rock fa-10x" id="r" onClick={playGame}></i>
<i className="choice fas fa-hand-scissors fa-10x" id='s' onClick={playGame}></i>
</div>
</div>
)
What do I need to change to set the state for both choice and score?
Since calls to setState are asynchronous, your two calls to setState are interfering with each other, the later one is overwriting the earlier one.
You have a few options.
Separate your state so that the values don't affect each other:
const [choices, setChoices] = useState({ user: null, cpu: null });
const [scores, setScores] = useState({ user: 0, cpu: 0);
Or go even further and set each of the two choices and two scores as their own state value.
Keep all state in one object, but update it all at once:
const setChoices = (userChoice, cpuChoice) => {
const cpuScore = gameItems.cpuScore + 1;
setGameItems({
...gameItems,
userChoice,
cpuChoice,
cpuScore
});
}
Use useReducer:
const initialState = {
userChoice: null,
userScore: 0,
cpuChoice: null,
cpuScore: 0
}
const [gameItems, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "UPDATE_CHOICES":
return {
...state,
userChoice: action.userChoice,
cpuChoice: action.cpuChoice
};
case 'UPDATE_CPU_SCORE':
return {
...state,
cpuScore: state.cpuScore + 1
}
default:
return state;
}
}, initialState);
const setChoices = (userChoice, cpuChoice) => {
dispatch({ type: 'UPDATE_CHOICES', userChoice, cpuChoice });
};
const cpuScore = () => {
dispatch({ type: 'UPDATE_CPU_SCORE'})
};
Basically, React doesn't immediately updates after you call setState.
If you call cpuScore() right after setChoices(), the cpuScore function you are calling is still the function from the current render, not after setChoices() update. Because of that, cpuScore() will set the state again, using the spread value of gameItems (which is still hasn't changed because the update from setChoices hasn't kicked in) which cause the changes by setChoices() to be overridden.
If React always immediately updates after every setState call, the performance would be atrocious. What React does is it will batch multiple setState calls into one update, so it doesn't update the DOM repeatedly.
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
My suggestion would be either to separate this two states, so they don't get overwritten by each other, or create a new function that handle all this updates in one place.
Related
I'm currently working on a poll app which is supposed to sequentially render a list of questions and post answers to the server. I have no problem handling answers but looping through questions gave me some trouble.
Here is a flow of my code:
PollContainer.js - component
import React, { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Question from './Questions/Question';
import { pushAnswers } from '../../actions/answers';
import { incrementCounter } from '../../actions/utils';
import Thanks from './Thanks'
const PollContainer = () => {
const dispatch = useDispatch();
const questions = useSelector(state => state.questions); // an array of questions
// a counter redux state which should increment at every click of 'submit' inside a question
const utils = useSelector(state => state.utils);
let activeQuestion = questions[utils.counter];
// function passed as a prop to a singular Question component to handle submit of an answer
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers;
answersUpdate.push(answer);
console.log(`counter:${utils.counter}`) // logs 0 everytime I click submit, while redux dev tools show that utils.counter increments at every click
if (utils.counter < questions.length ) {
setState({...state, answers: answersUpdate, activeQuestion: activeQuestion});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({...state, isFinished: true});
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer
})
return (
(utils.isLoading) ? (
<h1>Loading questions...</h1>
) : (
<div>
{(!state.isFinished && <Question { ...state }/>)}
{(state.isFinished && <Thanks/>)}
</div>
))
}
export default PollContainer;
incrementCounter action:
import * as types from "./types";
export const incrementCounter = () => {
return {
type: types.INCREMENT_COUNTER,
}
}
utils.js - reducer
// reducer handles what to do on the news report (action)
import * as types from '../actions/types';
const initialState = {
isLoading: false,
error: null,
counter: 0
}
export default (utils = initialState, action) => {
switch(action.type){
case types.LOADING_DATA:
return {...utils, isLoading: true};
case types.DATA_LOADED:
return {...utils, isLoading: false};
case types.ACTION_FAILED:
return {...utils, error: action.error};
case types.INCREMENT_COUNTER:
return {...utils, counter: utils.counter + 1} // here is the incrementing part
default:
return utils;
}
}
utils.counter that is passed to pushSingleAnswer function doesn't increment, however redux dev tools tells me it does increase every time I click submit in a Question component. Because of that it doesn't render next questions. The submit handler in Question component is simply this:
const handleSubmit = (e) => {
e.preventDefault();
props.pushSingleAnswer(state);
};
I also tried with:
useEffect(() => {
dispatch(incrementCounter())},
[state.answers]
);
expecting it'll increment every time there's an update to state.answers but it doesn't work either. Morover the counter in redux-dev-tools doesn't increment either.
I'd be very grateful for any suggestions, this is my first serious react-redux project and I really enjoy working with these technologies. However I do not quite understand how react decides to render stuff on change of state.
Issue
You are closing over the initial counter state in the pushSingleAnswer callback stored in state and passed to Question component.
You are mutating your state object in the handler.
Code:
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers; // <-- save state reference
answersUpdate.push(answer); // <-- state mutation
console.log(`counter:${utils.counter}`) // <-- initial value closed in scope
if (utils.counter < questions.length ) {
setState({
...state, // <-- persists closed over callback/counter value
answers: answersUpdate,
activeQuestion: activeQuestion,
});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer // <-- closed over in initial state
});
{(!state.isFinished && <Question { ...state }/>)} // <-- stale state passed
Solution
Don't store the callback in state and use functional state updates.
const pushSingleAnswer = (answer) => {
console.log(`counter:${utils.counter}`) // <-- value from current render cycle
if (utils.counter < questions.length ) {
setState(prevState => ({
...prevState, // <-- copy previous state
answers: [
...prevState.answers, // <-- copy previous answers array
answer // <-- add new answer
],
activeQuestion,
}));
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
});
{!state.isFinished && (
<Question { ...state } pushSingleAnswer={pushSingleAnswer} />
)}
I am attempting to create a hook which allows a component to subscribe to a part of the global state changing. For example, imagine my state looks like this
{
products: []
userForm: {
name: 'John Smith',
dateOfBirth: '07/10/1991'
}
}
The component which controls the dateOfBirth field in the userForm should only re-render if the dateOfBirth field changes.
Say I have some global state created using React context. Here is my attempt at subscribing to the field of the global state that that component cares about
function useField(field) {
const [globalState, setGlobalState] = useContext(GlobalState);
const value = globalState[field] || "initial";
const setValue = useCallback(
(value) => {
setGlobalState((state) => ({
...state,
[field]: value
}));
},
[setGlobalState, field]
);
return [value, setValue];
}
Demo https://codesandbox.io/s/dawn-fog-ieqxs?file=/src/App.js:326-612
The above code causes any component which uses the useField hook to rerender.
The desired behaviour is that the component should only rerender when that field changes.
It can work, but not with Context API, as for now Context API can't bailout of useless renders.
In other words: components subscribed to context provider will always render on provider value change.
An example of Context API known problem:
const GlobalContext = React.createContext(null);
const InnerComponent = () => {
/* eslint-disable no-unused-vars */
const { uselessState } = useContext(GlobalContext);
console.log(`Inner rendered`);
return <></>;
};
const InnerMemo = React.memo(InnerComponent);
const InnerComponentUsingContext = () => {
const { counter, dispatch } = useContext(GlobalContext);
console.log(`Inner Using Context rendered`);
return (
<>
<div>{counter}</div>
<button onClick={() => dispatch()}>Dispatch</button>
</>
);
};
const InnerComponentUsingContextMemo = React.memo(InnerComponentUsingContext);
const App = () => {
const [counter, dispatch] = useReducer((p) => p + 1, 0);
const [uselessState] = useState(null);
return (
<GlobalContext.Provider value={{ counter, uselessState, dispatch }}>
<InnerMemo />
<InnerComponentUsingContextMemo />
</GlobalContext.Provider>
);
};
That said, using every modern state management solution has a bailout function which will resolve this issue:
// Always renders
const [globalState, setGlobalState] = useContext(GlobalState);
const value = globalState[field] || "initial";
// Bailout, for example with redux
const value = useReducer(globalState => globalState[field], /* Can add bailout function here if necessary */);
I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
Updated Example:
https://codesandbox.io/s/wizardly-dust-fvxsl
Check if(!loaded){.... setTimeout
or
useEffect with [loaded]);
useEffect(() => {
const delay = Math.random() * 1000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("rendering multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [loaded]);
return <div>{loaded ? children : "loading"}</div>;
};
This is my first time working with react js , im trying to remove the alert when leaving this view cause i don't want to show it on the other view but in case that there is no error i want to keep the success alert to show it when i'm gonna redirect to the other view
but im getting this wearning on google chrome
Line 97:6: React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array react-hooks/exhaustive-deps
if i did include dispatch i get infinite loop
const [state, dispatch] = useUserStore();
useEffect(() => {
let token = params.params.token;
checktoken(token, dispatch);
}, [params.params.token]);
useEffect(() => {
return () => {
if (state.alert.msg === "Error") {
dispatch({
type: REMOVE_ALERT
});
}
};
}, [state.alert.msg]);
//response from the api
if (!token_valide || token_valide_message === "done") {
return <Redirect to="/login" />;
}
this is useUserStore
const globalReducers = useCombinedReducers({
alert: useReducer(alertReducer, alertInitState),
auth: useReducer(authReducer, authInitState),
register: useReducer(registerReducer, registerInitState),
token: useReducer(passeditReducer, tokenvalidationInitState)
});
return (
<appStore.Provider value={globalReducers}>{children}</appStore.Provider>
);
};
export const useUserStore = () => useContext(appStore);
UPDATE 09/11/2020
This solution is no longer needed on eslint-plugin-react-hooks#4.1.0 and above.
Now useMemo and useCallback can safely receive referential types as dependencies.#19590
function MyComponent() {
const foo = ['a', 'b', 'c']; // <== This array is reconstructed each render
const normalizedFoo = useMemo(() => foo.map(expensiveMapper), [foo]);
return <OtherComponent foo={normalizedFoo} />
}
Here is another example of how to safely stabilize(normalize) a callback
const Parent = () => {
const [message, setMessage] = useState('Greetings!')
return (
<h3>
{ message }
</h3>
<Child setter={setMessage} />
)
}
const Child = ({
setter
}) => {
const stableSetter = useCallback(args => {
console.log('Only firing on mount!')
return setter(args)
}, [setter])
useEffect(() => {
stableSetter('Greetings from child\'s mount cycle')
}, [stableSetter]) //now shut up eslint
const [count, setCount] = useState(0)
const add = () => setCount(c => c + 1)
return (
<button onClick={add}>
Rerender {count}
</button>
)
}
Now referential types with stable signature such as those provenients from useState or useDispatch can safely be used inside an effect without triggering exhaustive-deps even when coming from props
---
Old answer
dispatch comes from a custom hook so it doesn't have an stable signature therefore will change on each render (reference equality). Add an aditional layer of dependencies by wrapping the handler inside an useCallback hook
const [foo, dispatch] = myCustomHook()
const stableDispatch = useCallback(dispatch, []) //assuming that it doesn't need to change
useEffect(() =>{
stableDispatch(foo)
},[stableDispatch])
useCallback and useMemo are helper hooks with the main purpose off adding an extra layer of dependency check to ensure synchronicity. Usually you want to work with useCallback to ensure a stable signature to a prop that you know how will change and React doesn't.
A function(reference type) passed via props for example
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [])
}
Lets assume you have a child component which uppon mounting must set some state in the parent (not usual), the above code will generate a warning of undeclared dependency in useEffect, so let's declare setParentState as a dependency to be checked by React
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [setParentState])
}
Now this effect runs on each render, not only on mounting, but on each update. This happens because setParentState is a function which is recreated every time the function Component gets called. You know that setParentState won't change it's signature overtime so it's safe to tell React that. By wrapping the original helper inside an useCallback you're doing exactly that (adding another dependency check layer).
const Component = ({ setParentState }) =>{
const stableSetter = useCallback(() => setParentState(), [])
useEffect(() => setParentState('mounted'), [stableSetter])
}
There you go. Now React knows that stableSetter won't change it's signature inside the lifecycle therefore the effect do not need too run unecessarily.
On a side note useCallback it's also used like useMemo, to optmize expensive function calls (memoization).
The two mai/n purposes of useCallback are
Optimize child components that rely on reference equality to prevent unnecessary
renders. Font
Memoize expensive calculations
I think you can solve the problem at the root but that means changing useCombinedReducers, I forked the repo and created a pull request because I don't think useCombinedReducers should return a new reference for dispatch every time you call it.
function memoize(fn) {
let lastResult,
//initial last arguments is not going to be the same
// as anything you will pass to the function the first time
lastArguments = [{}];
return (...currentArgs) => {
//returning memoized function
//check if currently passed arguments are the same as
// arguments passed last time
const sameArgs =
currentArgs.length === lastArguments.length &&
lastArguments.reduce(
(result, lastArg, index) =>
result && Object.is(lastArg, currentArgs[index]),
true,
);
if (sameArgs) {
//current arguments are same as last so just
// return the last result and don't execute function
return lastResult;
}
//current arguments are not the same as last time
// or function called for the first time, execute the
// function and set last result
lastResult = fn.apply(null, currentArgs);
//set last args to current args
lastArguments = currentArgs;
//return result
return lastResult;
};
}
const createDispatch = memoize((...dispatchers) => action =>
dispatchers.forEach(fn => fn(action)),
);
const createState = memoize(combinedReducers =>
Object.keys(combinedReducers).reduce(
(acc, key) => ({ ...acc, [key]: combinedReducers[key][0] }),
{},
),
);
const useCombinedReducers = combinedReducers => {
// Global State
const state = createState(combinedReducers);
const dispatchers = Object.values(combinedReducers).map(
([, dispatch]) => dispatch,
);
// Global Dispatch Function
const dispatch = createDispatch(...dispatchers);
return [state, dispatch];
};
export default useCombinedReducers;
Here is a working example:
const reduceA = (state, { type }) =>
type === 'a' ? { count: state.count + 1 } : state;
const reduceC = (state, { type }) =>
type === 'c' ? { count: state.count + 1 } : state;
const state = { count: 1 };
function App() {
const [a, b] = React.useReducer(reduceA, state);
const [c, d] = React.useReducer(reduceC, state);
//memoize what is passed to useCombineReducers
const obj = React.useMemo(
() => ({ a: [a, b], c: [c, d] }),
[a, b, c, d]
);
//does not do anything with reduced state
const [, reRender] = React.useState();
const [s, dispatch] = useCombinedReducers(obj);
const rendered = React.useRef(0);
const [sc, setSc] = React.useState(0);
const [dc, setDc] = React.useState(0);
rendered.current++;//display how many times this is rendered
React.useEffect(() => {//how many times state changed
setSc(x => x + 1);
}, [s]);
React.useEffect(() => {//how many times dispatch changed
setDc(x => x + 1);
}, [dispatch]);
return (
<div>
<div>rendered {rendered.current} times</div>
<div>state changed {sc} times</div>
<div>dispatch changed {dc} times</div>
<button type="button" onClick={() => reRender({})}>
re render
</button>
<button
type="button"
onClick={() => dispatch({ type: 'a' })}
>
change a
</button>
<button
type="button"
onClick={() => dispatch({ type: 'c' })}
>
change c
</button>
<pre>{JSON.stringify(s, undefined, 2)}</pre>
</div>
);
}
function memoize(fn) {
let lastResult,
//initial last arguments is not going to be the same
// as anything you will pass to the function the first time
lastArguments = [{}];
return (...currentArgs) => {
//returning memoized function
//check if currently passed arguments are the same as
// arguments passed last time
const sameArgs =
currentArgs.length === lastArguments.length &&
lastArguments.reduce(
(result, lastArg, index) =>
result && Object.is(lastArg, currentArgs[index]),
true
);
if (sameArgs) {
//current arguments are same as last so just
// return the last result and don't execute function
return lastResult;
}
//current arguments are not the same as last time
// or function called for the first time, execute the
// function and set last result
lastResult = fn.apply(null, currentArgs);
//set last args to current args
lastArguments = currentArgs;
//return result
return lastResult;
};
}
const createDispatch = memoize((...dispatchers) => action =>
dispatchers.forEach(fn => fn(action))
);
const createState = memoize(combinedReducers =>
Object.keys(combinedReducers).reduce(
(acc, key) => ({
...acc,
[key]: combinedReducers[key][0],
}),
{}
)
);
const useCombinedReducers = combinedReducers => {
// Global State
const state = createState(combinedReducers);
const dispatchers = Object.values(combinedReducers).map(
([, dispatch]) => dispatch
);
// Global Dispatch Function
const dispatch = createDispatch(...dispatchers);
return [state, dispatch];
};
ReactDOM.render(<App />, 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>
Since React hooks rely on the execution order one should generally not use hooks inside of loops. I ran into a couple of situations where I have a constant input to the hook and thus there should be no problem. The only thing I'm wondering about is how to enforce the input to be constant.
Following is a simplified example:
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = constantIdArray.map((id) => useCallback(() => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
}));
return { state, callbacks };
}
const idArray = ['id-1', 'id-2', 'id-3'];
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput(idArray);
return (
<div>
<div onClick={callbacks[0]}>
{state[0]}
</div>
<div onClick={callbacks[1]}>
{state[1]}
</div>
<div onClick={callbacks[2]}>
{state[2]}
</div>
</div>
)
}
Is there a pattern for how to enforce the constantIdArray not to change? My idea would be to use a creator function for the hook like this:
const createUseHookWithConstantInput = (constantIdArray) => () => {
...
}
const idArray = ['id-1', 'id-2', 'id-3'];
const useHookWithConstantInput = createUseHookWithConstantInput(idArray)
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput();
return (
...
)
}
How do you solve situations like this?
One way to do this is to use useEffect with an empty dependency list so it will only run once. Inside this you could set your callbacks and afterwards they will never change because the useEffect will not run again. That would look like the following:
const useHookWithConstantInput = (constantIdArray) => {
const [state, changeState] = useState({});
const [callbacks, setCallbacks] = useState([]);
useEffect(() => {
changeState(buildInitialState(constantIdArray));
const callbacksArray = constantIdArray.map((id) => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
});
setCallbacks(callbacksArray);
}, []);
return { state, callbacks };
}
Although this will set two states the first time it runs instead of giving them initial values, I would argue it's better than building the state and creating new callbacks everytime the hook is run.
If you don't like this route, you could alternatively just create a state like so const [constArray, setConstArray] = useState(constantIdArray); and because the parameter given to useState is only used as a default value, it'll never change even if constantIdArray changes. Then you'll just have to use constArray in the rest of the hook to make sure it'll always only be the initial value.
Another solution to go for would be with useMemo. This is what I ended up implementing.
const createCallback = (id, changeState) => () => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
};
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = useMemo(() =>
constantIdArray.map((id) => createCallback(id, changeState)),
[],
);
return { state, callbacks };
};