Wondering what the best method is to handle callbacks that get passed to children/into custom hooks that are used inside useEffect blocks (or any hook with dependency arrays)
This is going by the assumption we don't have access to that callback to wrap in a useCallback or define it outside of the parent/changing scope ourselves.
Is there better ways than suggested below? Do I need to worry about stale fns/closures?
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = (value) => {
setValue(value);
}
return (
<Child onChange={onChange} />
)
}
function Child({ onChange }) {
useEffect(() => {
// ...
onChange(changingValue);
}, [changingValue, onChange]); // <- always changing
}
// Is there a drawback to this approach?
function Child({ onChange }) {
const callbackRef = useRef();
callbackRef.current = onChange;
useEffect(() => {
// ...
callbackRef.current(changingValue);
}, [changingValue])
}
// or should it be updated in useLayoutEffect?
function Child({ onChange }) {
const callbackRef = useRef();
useLayoutEffect(() => {
callbackRef.current = onChange;
});
useEffect(() => {
// ...
callbackRef.current(changingValue);
}, [changingValue]);
}
I think the best is the first one, it's simpler, and if you use a useCallback where you define onChange, then the function will be called just one time every time the value changes, like this:
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((input) => {
setValue(input);
}, []);
return (
<Child onChange={onChange} />
)
}
function Child({ onChange }) {
const [changingValue, setChangingValue] = useState('');
useEffect(() => {
onChange(changingValue);
}, [changingValue, onChange]);
return (
<input type="text" value={changingValue} onChange={(event) => setChangingValue(event.target.value)} />
)
}
By the way, in this particular case, of course you can call the function directly on the onChange, passing the value, like this:
function Parent() {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((input) => {
setValue(input);
}, []);
return (
<Child onChange={onChange} value={value} />
)
}
function Child({ onChange, value }) {
return (
<input type="text" value={value} onChange={(event) => onChange(event.target.value)} />
)
}
Related
Container
import { InputField } from './InputField';
const sleep = (time: number) => new Promise((res) => setTimeout(res, time, ''));
export const Container = () => {
const [inputValue, setInputValue] = React.useState('');
React.useEffect(() => {
(async () => await sleep(1000))();
async function fetchMyAPI(time, value) {
await sleep(time);
setInputValue(value);
}
fetchMyAPI(1000, 'vbc1');
fetchMyAPI(2000, 'dgi1');
}, []);
const inputChange = (value) => {
setInputValue(value);
};
return <InputField inputValue={inputValue} inputChange={inputChange} />;
};
InputField
export const InputField = ({
inputValue,
inputChange,
}: {
inputValue: string;
inputChange: (value: string) => void;
}) => {
const [value, setValue] = React.useState('');
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
const handleChange = (event) => {
setValue(event.target.value);
inputChange(event.target.value + '1');
};
return <input value={value} onChange={handleChange} />;
};
inputValue above can change multiple times.
also a local variable in input is used to display , and inputValue is slightly different from it. So when we keep track of InputValue , we pass the cleared data to the local variable. And vice versa, we modify the data to put in the inputValue.
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
Every time we call handleChange : we do setValue and inputChange. Thus, we change the value variable and the inputValue variable. After the inputValue is changed, useEffect is called which observes the inputValue. And overwrites exactly the same value of the Value variable. This is problem!
What is the correct solution to this problem?
You can create a boolean state effectRan to track whether the effect already ran or not, and only invoke the effect's logic if effectRan == false, then set it to true.
When the effect runs again with it as true, have it set it back to false to prepare to run again in the next change.
I changed the code a bit to highlight the approach:
const {useState, useEffect } = React
const InputField = () => {
const [value, setValue] = React.useState('');
const [effectRan, setEffectRan] = useState(true);
React.useEffect(() => {
if (!effectRan) {
setValue(prev => prev + '-');
setEffectRan(true)
console.log('Effect just ran');
} else {
setEffectRan(false)
}
}, [value]);
const handleChange = (event) => {
setValue(event.target.value);
};
return <input onChange={handleChange} value={value} />;
};
ReactDOM.render(<InputField />, root)
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="root"></div>
function Foo() {
const [state, setState] = useState(0);
const cb = useCallback(debounce(() => {
console.log(state);
}, 1000), []);
return ...;
}
In this example, state can become stale in the callback. One way I can think of to fix this is something like:
function Foo() {
const [state, setState] = useState(0);
const cbHelper = useCallback(debounce((state2) => {
console.log(state2);
}, 1000), [])
const cb = () => cbHelper(state);
return ...;
}
However, this looks pretty messy. Is there a cleaner/better way to do this?
Edit:
I can't just do the following because debounce won't work:
useCallback(debounce(() => {
console.log(state);
}, 1000), [state]);
I've used use-debounce to manage that
import { useDebouncedCallback } from 'use-debounce';
function Input({ defaultValue }) {
const [value, setValue] = useState(defaultValue);
// Debounce callback
const [debouncedCallback] = useDebouncedCallback(
// function
(value) => {
setValue(value);
},
// delay in ms
1000
);
// you should use `e => debouncedCallback(e.target.value)` as react works with synthetic evens
return (
<div>
<input defaultValue={defaultValue} onChange={(e) => debouncedCallback(e.target.value)} />
<p>Debounced value: {value}</p>
</div>
);
}
I want to do a debounce for custom input, but my problem is I can't stop useEffect from trigger on initial render
import { useDebouncedCallback } from "use-debounce";
interface myInputProps {
getValue: any;
}
const MyInput = ({ getValue }: myInputProps) => {
const [value, setValue] = useState("");
React.useEffect(() => {
getValue(value);
}, [value]);
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
export default function App() {
const [debouncedCallback] = useDebouncedCallback(value => {
console.log(value);
}, 1000);
return (
<div className="App">
<MyInput getValue={debouncedCallback} />
</div>
);
}
https://codesandbox.io/s/upbeat-lamport-ukq70?file=/src/App.tsx
I've also tried useLayoutEffect but it doesn't solve the problem.
We could use useRef to keep track of if it's the first time the useEffect hook is being run.
https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
Sandbox link: https://codesandbox.io/s/confident-cerf-flkf2?file=/src/App.tsx
const MyInput = ({ getValue }: myInputProps) => {
const [value, setValue] = useState("");
const first = useRef(true);
React.useEffect(() => {
if (first.current) {
first.current = false;
return;
}
getValue(value);
}, [value]);
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
Set initial value to undefined and you can explicitly check for undefined. Once the user enter, it won't be undefined.
const MyInput = ({ getValue }: myInputProps) => {
const [value, setValue] = useState(undefined);
React.useEffect(() => {
if (value === undefined) {
return;
}
getValue(value);
}, [value]);
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
I'm using React right now and I'm trying to get my localstorage to update a state once the event handles a return on search and then hold that state until the next search is completed. Right now I can't figure out where to put an event handler that triggers the correct state and holds the correct value.
const useStateWithLocalStorage = localStorageKey => {
const [value, setValue] = React.useState(
localStorage.getItem(localStorageKey) || ''
);
React.useEffect(() => {
localStorage.setItem(localStorageKey, value);
}, [value]);
return [value, setValue];
};
export default function App() {
const [value, setValue] = useStateWithLocalStorage(
'myValueInLocalStorage'
);
const onChange = event => setValue(event.target.value);
const [state, setState] = useState({
message: 'test deploy',
results: [],
value: '',
});
...
and where I'm trying to implement the event handler
export default function SearchAppBar(props) {
const classes = useStyles();
const [searchTerm, setSearchTerm] = useState('');
const { onClick } = props;
...
<InputBase
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
inputProps={{ 'aria-label': 'search' }}
/>
<Button onClick={() => onClick(searchTerm)}> Search </Button>```
Hereby my solution. I've created an useLocalStorage function that stores and gets or sets items in the local storage and holds them in its own state:
import React from "react";
export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = React.useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = value => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
};
export default useLocalStorage;
For the searchBar component I've used a forwardRef to access the value of the input inside our higher component App. The newSearch function and searchTerm variable are destructured off the props. The placeholder holds the stored value in localStorage, which is searchTerm:
export const SearchAppBar = React.forwardRef(
({ newSearch, searchTerm }, ref) => {
return (
<>
<input ref={ref} type="text" placeholder={searchTerm} />
<button onClick={newSearch}> Search </button>
</>
);
}
);
Inside the main App component I'm using our useLocalStorage function hook to get and set the search. Inside newSearch I'm updating the search term by calling our hook with the value of the forwarded input ref.
export default function App() {
const ref = React.createRef();
const [searchTerm, setSearchTerm] = useLocalStorage(
"search",
"Not searched yet"
);
const newSearch = () => {
setSearchTerm(ref.current.value);
};
return (
<>
<SearchAppBar ref={ref} newSearch={newSearch} searchTerm={searchTerm} />
<p>Last search: {searchTerm}</p>
</>
);
}
Hope this is a workable solution for you.
Please find a code snippet here:
https://codesandbox.io/s/cranky-sunset-8fqtm?file=/src/index.js:387-773
I like the approach used by redux to handling the states on react. I use redux with redux-persist library to save the state instead of localStorage. If your project grows and you need to work with more complex states, it could help you.
I have a set of buttons in a child component where when clicked set a corresponding state value true or false. I have a useEffect hook in this child component also with dependencies on all these state values so if a button is clicked, this hook then calls setFilter which is passed down as a prop from the parent...
const Filter = ({ setFilter }) => {
const [cycling, setCycling] = useState(true);
const [diy, setDiy] = useState(true);
useEffect(() => {
setFilter({
cycling: cycling,
diy: diy
});
}, [cycling, diy]);
return (
<Fragment>
<Row>
<Col>
<Button block onClick={() => setCycling(!cycling)}>cycling</Button>
</Col>
<Col>
<Button block onClick={() => setdIY(!DIY)}>DIY</Button>
</Col>
</Row>
</Fragment>
);
};
In the parent component I display a list of items. I have two effects in the parent, one which does an initial load of items and then one which fires whenever the filter is changed. I have removed most of the code for brevity but I think the ussue I am having boils down to the fact that on render of my ItemDashboard the filter is being called twice. How can I stop this happening or is there another way I should be looking at this.
const ItemDashboard = () => {
const [filter, setFilter] = useState(null);
useEffect(() => {
console.log('on mount');
}, []);
useEffect(() => {
console.log('filter');
}, [filter]);
return (
<Container>..
<Filter setFilter={setFilter} />
</Container>
);
}
I'm guessing, you're looking for the way to lift state up to common parent.
In order to do that, you may bind event handlers of child components (passed as props) to desired callbacks within their common parent.
The following live-demo demonstrates the concept:
const { render } = ReactDOM,
{ useState } = React
const hobbies = ['cycling', 'DIY', 'hiking']
const ChildList = ({list}) => (
<ul>
{list.map((li,key) => <li {...{key}}>{li}</li>)}
</ul>
)
const ChildFilter = ({onFilter, visibleLabels}) => (
<div>
{
hobbies.map((hobby,key) => (
<label {...{key}}>{hobby}
<input
type="checkbox"
value={hobby}
checked={visibleLabels.includes(hobby)}
onChange={({target:{value,checked}}) => onFilter(value, checked)}
/>
</label>))
}
</div>
)
const Parent = () => {
const [visibleHobbies, setVisibleHobbies] = useState(hobbies),
onChangeVisibility = (hobby,visible) => {
!visible ?
setVisibleHobbies(visibleHobbies.filter(h => h != hobby)) :
setVisibleHobbies([...visibleHobbies, hobby])
}
return (
<div>
<ChildList list={visibleHobbies} />
<ChildFilter onFilter={onChangeVisibility} visibleLabels={visibleHobbies} />
</div>
)
}
render (
<Parent />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Yes, you can, useEffect in child component which depends on the state is also how you typically implement a component which is controlled & uncontrolled:
const NOOP = () => {};
// Filter
const Child = ({ onChange = NOOP }) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
onChange(counter);
}, [counter, onChange]);
const onClick = () => setCounter(c => c + 1);
return (
<div>
<div>{counter}</div>
<button onClick={onClick}>Increase</button>
</div>
);
};
// ItemDashboard
const Parent = () => {
const [value, setState] = useState(null);
useEffect(() => {
console.log(value);
}, [value]);
return <Child onChange={setState} />;
};