How to implement setState callback patten with useState - javascript

I have multiple setState(newState, callback) statements in my Class component. Now that I'm shifting to using hooks, I am unable to put all of the callbacks in a useEffect and call them conditionally.
Some setState calls are not distinguishable with regard to what changes they are making (they may be changing the same array stored in the useState) but they all fire very different callbacks. I cannot simply just put different conditions in useState and fire callbacks.
The whole logic is becoming much more complex. How have you handled changing setState to useState without affecting the callbacks or having to make very complex logic changes inside useEffect?
Example code:
resetSelections = () => {
const { filterData, toggleSidebarOnReset, toggleSidebar } = this.props;
this.setState(
{
selections: getSelectionsFromFilterData(filterData),
},
() => {
this.triggerAdvancedFilter();
if (toggleSidebarOnReset && toggleSidebar) {
toggleSidebar();
}
if (this.props.removeFilterDefaultSelection) {
this.props.removeFilterDefaultSelection();
}
}
);
};
addCustomField = filterGroupData => {
this.setState(
prevState => ({
customSelectionsMap: {
...prevState.customSelectionsMap,
[filterGroupData.name]: filterGroupData.id,
},
selections: {
...prevState.selections,
[filterGroupData.name]: [],
},
}),
() => this.props.addCustomFieldInFilterData(filterGroupData)
);
};
removeCustomField = data => {
const { selections, customSelectionsMap } = this.state;
const newSelections = { ...selections };
const newCustomSelectionsMap = { ...customSelectionsMap };
delete newSelections[data.name];
delete newCustomSelectionsMap[data.name];
this.setState(
{
selections: newSelections,
customSelectionsMap: newCustomSelectionsMap,
},
() => {
this.props.removeCustomFieldFromFilterData(data);
this.triggerAdvancedFilter();
}
);
};
addToSelection = ({ group, name }, isReplace) => {
const { selections } = this.state;
if (R.contains(name, selections[group])) return null;
const pushState = isReplace ? '$set' : '$push';
this.setState(
prevState => ({
selections: update(prevState.selections, {
[group]: { [pushState]: [name] },
}),
}),
() => this.triggerAdvancedFilter()
);
};

You can apply your callback implementation in useEffect by giving your state variable in dependency array
You can refer to this also
How to use `setState` callback on react hooks

Related

Value of variable outside of useEffect hook has old data

What the code does: It's performing a DOM search based on what's typed in an input (it's searching elements by text). All this is happening in a React component.
import { useEffect, useReducer } from "react";
let elements: any[] = [];
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
elements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
console.log('elements 1:', elements)
}, [state]);
console.log('elemets 2:', elements)
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
The problem: The value of elements outside of useEffect is the old data. For example, if you type a in the input, console.log('elements 1:', elements) will log 1, and console.log('elements 2:', elements) will log 0. Note: there are 3 buttons, and one of them has the text a.
The strange thing is that the problem doesn't happen if you comment out this if-statement:
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
In this case, if you type anything (since the pattern matching has been commented out), console.log('elements 1:', elements) and console.log('elements 2:', elements) will log 3. Note: there are 3 buttons.
Question: What could be the problem, and how to fix it? I want to render the current length of elements.
Live code:
It's happening because of the elements variable is not a state, so it's not reactive.
Create a state for the elements:
const [elements, setElements] = useState<HTMLButtonElement[]>([])
And use this state to handle the elements.
import { useEffect, useReducer, useState } from "react";
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
const [elements, setElements] = useState<HTMLButtonElement[]>([])
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
let newElements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
setElements(newElements)
console.log("elements 1:", elements?.length);
}, [state]);
console.log("elemets 2:", elements?.length);
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements?.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
Your useEffect() runs after your component has rendendered. So the sequence is:
You type something into input, that triggers handleInputChange
handleInputChange then updates your state using updateState()
The state update causes a rerender, so App is called App()
console.log('elemets 2:', elements.length) runs and logs elements as 0 as it's still empty
App returns the new JSX
Your useEffect() callback runs, updating elements
Notice how we're only updating the elements after you've rerendered and App has been called.
The state of your React app should be used to describe your UI in React. Since elements isn't React state, it has a chance of becoming out of sync with the UI (as you've seen), whereas using state doesn't have this issue as state updates always trigger a UI update. Consider making elements part of your state. If it needs to be accessible throughout your entire App, you can pass it down as props to children components, or use context to make it accessible throughout all your components.
With that being said, I would make the following updates:
Add elements to your state
Remove your useEffect() with the dependency of [state]. If we were to update the elements state within this effect, then that would trigger another rerender directly after the one we just did for the state update. This isn't efficient, and instead, we can tie the update directly to your event handler. See You Might Not Need an Effect for more details and dealing with other types of scenarios:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useEffect, useReducer} = React;
const App = () => {
const initialState = {keyEvent: {}, value: "Initial state", elements: []};
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates}),
initialState
);
function searchDOM(value) {
const selectors = "button";
const pattern = new RegExp(value === "" ? "^$" : value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
return nodeWithText?.textContent?.match(pattern);
}
return false;
}
);
}
function handleInputChange(event) {
updateState({
value: event.target.value,
elements: searchDOM(event.target.value)
});
}
function isCommand(event) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event) {
if (isCommand(event)) {
updateState({
keyEvent: event
});
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
console.log("elements:", state.elements.length);
return (
<div>
<input id="input" type="text" onChange={handleInputChange} value={state.value} />
<div id="count">{state.elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
ReactDOM.createRoot(document.body).render(<App />);
</script>
useEffect triggered after react completed its render phase & flush the new changes to the DOM.
In your case you have two useEffects. The first one register your event lister which will then update your component state when input field change. This triggers a state update.( because of the setState )
So React will start render the component again & finish the cycle. And now you have 2nd useEffect which has state in dependency array. Since the state was updated & the new changes are committed to the DOM, react will execute 2nd useEffect logic.
Since your 2nd useEffect just assign some values to a normal variable React will not go re render your component again.
Based on your requirement you don't need a 2nd useEffect. You can use a useMemo,
let elements = useMemo(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
})
}, [state])
Note: You don't need to assign your elements into another state. It just create another unwanted re render in cycle. Since you are just doing a calculation to find out the element array you can do it with the useMemo

State not captured by callback in React component

I have this component that uses useState, and wraps its children in a Modal component, which is an old component from our application, and the modal component happens to be a total nightmare. But it takes an acceptData object which has a callback prop for when the "accept" button is clicked.
My issue is that my component is getting its state updated via another child (see onFileReadComplete), but then later when the button is clicked, that state is null. I can even see in the React dev tools that the state is not null, it has the value I expect.
Here is the component:
const CSVEditSwitcher = ({ acceptData, hideData }) => {
const [parsedData, setParsedData] = useState(null);
const dispatch = useDispatch();
const enterLineups = lineupObject => dispatch(csvEditAPI(lineupObject));
const modalProps = {
className: "csv-upload",
commonHide: true,
acceptData: {
actionName: "Submit",
className: acceptData.className,
callback: () => {
if (!parsedData) {
console.log("no data!"); // we keep getting here!
return;
}
enterLineups(parsedData.masterLineups).then(acceptData.callback);
},
},
hideData,
validator: () => true,
};
return (
<Modal {...modalProps}>
{!parsedData ? (
<UploadEditTemplate
onFileReadComplete={data => {
console.log({ data }); // this is always correct!
setParsedData(data);
}}
/>
) : (
<ImportResults {...parsedData} />
)}
</Modal>
);
};
In case it's some crazy this issue, I've tried replacing the callback with a function () { } syntax, to no avail.
There's nothing interesting in the call point for the callback in Modal:
accept(e) {
if (!e) return;
if (e.key && (e.key !== "Enter" || e.key !== "Return")) return;
const { acceptCallback } = this.state;
console.log(acceptCallback, e);
if (acceptCallback) acceptCallback(e);
}
Update
As a hail-mary, I tried converting this to a class component (wrapping it in a functional component that provides the hook values). It actually works, but only when accessing this.state.parsedData explicitly in the callback. i.e., I have const { parsedData } = this.state at the top of the render method for the other uses of that value, but the destructured variable does not work in the callback.
I would love if anyone could lend insight on what the hell is going on here!
While destructuring props in injection ({...modalProps}...), you create a new instance in memory. Therefore callback prop will always have initial parsedData value.
My suggestion is to use callback as a reference to useCallback which aware to state manipulation
const callback = useCallback(() => {
if (!parsedData) {
console.log("no data!")
return;
}
enterLineups(parsedData.masterLineups).then(acceptData.callback);
}, [parsedData])
const modalProps = {
className: "csv-upload",
commonHide: true,
acceptData: {
actionName: "Submit",
className: acceptData.className,
callback
},
hideData,
validator: () => true,
};

React Context - State value is not up-to-date inside a function

I have the following context:
import React, { createContext, useState } from "react";
const OtherUsersContext = createContext(null);
export default OtherUsersContext;
export function OtherUsersProvider({ children }) {
const [otherUsers, setOtherUsers] = useState(new Map([]));
const addUser = (userId, userData) => {
setOtherUsers(
(prevOtherUsers) => new Map([...prevOtherUsers, [userId, userData]])
);
};
const updateUser = (userId, userData, merge = true) => {
...
};
const getUser = (userId) => otherUsers.get(userId);
const resetUsers = () => {
setOtherUsers(new Map([]));
};
return (
<OtherUsersContext.Provider
value={{
addUser,
updateUser,
getUser,
resetUsers,
}}
>
{children}
</OtherUsersContext.Provider>
);
}
In my app, when a user signs out, I need to reset this context's map, using the function "resetUsers".
Currently this is working good, but there has no sense to reset the map if it has no values, so I have changed the "resetUsers" function to:
const resetUsers = () => {
if(otherUsers.size) {
setOtherUsers(new Map([]));
}
}
And, this is not working good, because inside resetUsers, otherUsers.size is always 0. Something which disturbs me because outside the function, the value is the correct one...
...
const resetUsers = () => {
console.log(otherUsers.size); // 0
setOtherUsers(new Map([]));
};
console.log(otherUsers.size); // 5
return ( ...
Any ideas?
The functional updates part of the hooks docs. says:
If the new state is computed using the previous state, you can pass a function to setState.
So instead of just passing the new value to your setter, you can pass a function that depends on the previous state.
This means that you can do:
const resetUsers = () => {
setOtherUsers(prevOtherUsers => prevOtherUsers.size ? new Map([]): prevOtherUsers);
}
One tip, if you are not getting the most updated state value inside a function, then wrap it inside an useCallback.
Try this:
const resetUsers = useCallback(() => {
if (otherUsers.size > 0) {
console.log(otherUsers.size); // 5
setOtherUsers(new Map([]));
}
}, [otherUsers]);

Why useEffect is not called, when props have changed, render is called?

I have useEffect and I don't understand why useEffect is not called, when props have changed, but render is called. I have something like this, 'block' is object:
const { block } = props;
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
});
}, []);
Thank you for helping! Now, I have next question) My inputRef is null, when block is changed and render is called:
const { block } = props;
const inputRef = useRef(null);
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
});
}, [block, inputRef]);
...
<SomeComponent
inputRef={inputRef}
/>
Because you have no parameters on your useEffect. If you pass the second parameter as an empty array, it will only fire when the component is mounted.
Here is a reference from the official docs
You can get this behavior by passing a block property as the second parameter in your useEffect function:
const { block } = props;
useEffect(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
});
}, [block]);
Don't forget that you can pass more properties:
useEffect(() => {}, [block, otherProperty])

React AutoSuggest: search text box not updating on suggestion selection

I am using the react autosuggest library to build auto-suggestion
import Autosuggest from "react-autosuggest";
import React, { Component } from "react";
import QueryString from "query-string";
class AutoSuggestSearch extends Component {
constructor() {
super();
this.state = {
value: "",
suggestions: []
};
this.getSuggestionValue = this.getSuggestionValue.bind(this);
this.renderSuggestion = this.renderSuggestion.bind(this);
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
getSuggestionValue = suggestion => suggestion.fullNameSuggestion;
renderSuggestion = suggestion => <div>{suggestion.name}</div>;
onSuggestionSelected = (event, { suggestion}) => {
console.log(suggestion);
this.setState({
suggestions: [],
value: suggestion.name
});
};
onSuggestionsFetchRequested = ({ value }) => {
const params = {
stationPrefixName: value
};
const queryParams = QueryString.stringify(params);
fetch(`http://localhost:8000/api/suggest?${queryParams}`)
.then(res => res.json())
.then(data => {
console.log(data);
this.setState({
suggestions: data
});
})
.catch(console.log);
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: [],
value: ""
});
};
render() {
const { value, suggestions } = this.state;
const inputProps = {
placeholder: "Search",
value,
onChange: this.onChange
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
inputProps={inputProps}
/>
);
}
}
export default AutoSuggestSearch;
The suggestion gets rendered on typing on search box as well as the logging inside onSuggestionSelected gets logged correctly but the input search box does not update correctly.
On debugging further I found that onSuggestionsClearRequested also gets invoked after onSuggestionSelected which is causing the search input box to be empty.
I validated this by adding const string inside onSuggestionsClearRequested
onSuggestionsClearRequested = () => {
alert("clear request");
this.setState({
suggestions: [],
value: "mysearch"
});
};
Is there anyway to prevent onSuggestionsClearRequested invokation on suggestion selection?
Or updating the search query value inside onSuggestionsClearRequested is the correct way?
You can use componentDidUpdate or UseEffect if you are using it in functional component.
I have used react-autosuggest in functional component and clear suggestion works only if value doesn't matches with the suggestions:
const [clear, setClear] = useState(false);
const handleOnChange = newValue => {
setValue(newValue);
setClear(false);
};
useEffect(() => {
if (!suggestions.some(option => option === value) && clear) {
setValue('');
}
}, [clear]);
const onSuggestionsClearRequested = () => {
setClear(true);
setSuggestions([]);
};
The onSuggestionsClearRequested function gets called everytime you click outside the search input, which is the default implementation of the libary being used,
What we implement in onSuggestionsClearRequested is upto us.
you can change the implementation as follows :
Approach keep keyword inside input if available options are not selected
onSuggestionsClearRequested = () => {};
this should provide the desired implementation behaviour.
Hi you may approach with hooks. It looks good and less coding.
You may find below
https://github.com/rajmaravanthe/react-auto-suggestion

Categories