I'm developing a React app without Redux or any other state manager.
Let's say I want to do three things when a button is clicked:
Enable some other button
Remove a label
Show a confirmation toaster
These 3 things are controlled by 3 variables of the state. I could therefore do simply this:
myHandler = () => {
this.setState({
canSave: true, // Enable the button
isLabelVisible: false, // Hide label
isConfirmationMessageVisible: true, // Show confirmation message
});
}
However, I could get rid of those comments by using some private class functions, like this:
myHandler = () => {
this.toggleSaveButton(true);
this.toggleLabel(false);
this.toggleConfirmationMessage(true);
}
toggleSaveButton= (enabled) => {
this.setState({
canSave: enabled,
});
}
toggleLabel= (visible) => {
this.setState({
isLabelVisible: visible,
});
}
toggleConfirmationMessage= (visible) => {
this.setState({
isConfirmationMessageVisible: visible,
});
}
In addition to remove those comments which could easily get out-of-sync with the code, this allows me to reuse the private methods in other places of my code.
Since this is handled in a synthetic event, I have read here that it will be batched, so I can expect no performance penalty.
My question is: is this good practice? have you used this approach? can you point some potential drawbacks I can not foresee right now?
This is perfectly fine. As you mention, React batches all updates to the state that are triggered from event handlers. This means that you can safely use multiple setState() like you are doing here.
In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.
The only thing you need to look out for is if you are changing the same state twice from two setState() calls. For example:
a() {
this.setState({foo: foo+1});
}
b() {
this.setState({foo: foo+1});
}
Calling a() and then b() from the same event, will increment foo by 1, not two.
Instead use:
a() {
this.setState(prevState => ({foo: foo+1}));
}
b() {
this.setState(prevState => ({foo: foo+1}));
}
this will correctly increment foo by 2.
For potential future readers who are not calling multiple setState() from an event handler, I should note the following:
With current version, several setStates outside of event handlers (e.g. in network responses) will not be batched. So you would get two re-renders in that case.
Alternative solution
What you can do though, regardless if you call setState() from an event handler or not, is to build an object and then set it as the new state. The potential benefit here is that you only set the state once and thus don't rely on batching or where the function was triggered from (event handler or not).
So something like:
myHandler = () => {
let obj = {}
obj = this.toggleSaveButton(obj, true);
obj = this.toggleLabel(obj, false);
obj = this.toggleConfirmationMessage(obj, true);
this.setState(obj);
}
toggleSaveButton= (obj, enabled) => {
obj.canSave = enabled;
return obj;
}
toggleLabel= (visible) => {
obj.isLabelVisible = visible;
return obj;
}
toggleConfirmationMessage= (visible) => {
obj.isConfirmationMessageVisible = visible;
return obj;
}
Related
In #lexical/react, is there a substantial penalty (performance or other) for registering editor commands in a useEffect with no dependency array?
useEffect(() => {
const unsubscribe = editor.registerCommand(
KEY_ENTER_COMMAND,
event => { /* ... */ },
COMMAND_PRIORITY_HIGH
)
return unsubscribe
})
Is this internally demanding for Lexical, or is it merely a question of calling an extra simple function? Or are there maybe some other downsides to this approach?
It's fairly cheap, behind the scenes we just add/delete from a Map and a Set. But it's cheaper if you have to do none of this.
useCommandSubscription is an OK abstraction, some (untested) code:
function useCommandSubscription<T>(command: LexicalCommand<T>, fn: CommandListener<T>, priority: CommandListenerPriority): void {
const [editor] = useLexicalComposerContext();
useLayoutEffect(() => {
return editor.registerCommand(command, fn, priority);
}, [editor]);
}
useCommandSubscription(FOO_COMMAND, () => { ... }, priority);
But note how you can further optimize what we provide out of the box:
useEffect(() => {
// You can return immediately, no need to store the cleanup function in a variable
return editor.registerCommand(...);
}, [editor]);
A common use case is that you listen to multiple commands/updates at once, you can leverage mergeRegister (from #lexical/utils):
useEffect(() => {
return mergeRegister(
editor.registerCommand(..),
editor.registerCommand(..),
editor.registerUpdateListener(..),
}, [editor]);
Side note: Beware when listening to the key enter command. Android works with composition and will not trigger a key enter event. Depending on your use case you may want to explore INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, transforms based on LineBreakNode or ParagraphNode or mutation listeners based on these two nodes.
I have the following function to set state:
const [study, setStudy] = useState(defaultState)
const setValue = (section, key, value) => {
setStudy({...study, [section]: {...study[section], [key]: value}})
}
But for some reason it keeps overriding the existing state, what am I doing wrong?
I call the function like this:
setValue('company', 'name', 'test')
setValue('property', 'state', 'test2')
setValue('property', 'address', 'test3')
Structure of data:
const defaultState: StudyData = {
client: {},
company: {},
property: {},
study: {}
}
I think the problem is that the study value you destruct is still an old version (a memoized one). It is the same between all setValue calls and thus your first two setValue calls do essentially nothing. If this is what's happening to you, the best way to fix it would be to give a callback function to setStudy. Any function given to a setter will get the absolute latest version of the state as parameter. Try it like this:
setStudy((latestStudy) => {
return {
...latestStudy,
[section]: {...latestStudy[section], [key]: value}
}
});
When dealing with multiple state-updating events, the standard behavior for React is to batch them. The state-updating method does not immediately update the state of the component, React just puts the update in queue to be processed later when event handling is complete. Changes are all flushed together at the end of the event and you don't see the intermediate state.
With batching, when the final set-state event executes, it has no recollection that the prevState has been updated (asynchrounous), which is why the final state appears to only reflect changes made by the last event.
Using a call-back function as the first argument for your setStudy method will help you return the intermediate states and avoid the issue of batching. The updater function will loop through and shallowly merge all updated states with the previous component state. This ensures that we receive all state-updates and use all intermediate states before ultimately arriving at the final update.
const setValue = (section, key, value) => {
setStudy(study => {
return {
...study,
[section]: { ...study[section], [key]: value }
};
});
};
Also see working sandbox: https://codesandbox.io/s/delicate-tree-vqrz7
There is a pattern that I use often that I feel must be an anti-pattern but I don't know a better alternative.
Occasionally my components may receive one or more events that mean that a re-render is necessary. Sometimes it is the case that I do not know how many times the event handlers will be called. To prevent multiple re-renders as a result of many calls to the handlers I do something like this:
_myEventHandler() { // may be called multiple times between renders
if (!this._updateQueued) {
this._updateQueued = true;
this._updateTimer = setTimeout(() => {
this._updateQueued = false;
this.forceUpdate();
}, 0);
}
}
The problem here is I feel it can't be performant due to the latency between code stopping and the event loop starting.
A real world example of this is when I am using react-visibility-sensor and I have multiple elements change their visibility at once, I don't want to re-render for each element, instead I want just one re-render once all the updates have been received.
Is there another better way to deal with multiple calls?
BTW: if you are going to use the above hack don't forget to call clearTimout(this._updateQueued) in your componentWillUnmount
A debounce will reduce the number of times a certain piece of code is run, regardless of how often it is called. Here is a rather simple implementation.
const debounce = (callable, time) => {
let timeout;
return function() {
const functionCall = () => callable.apply(this, arguments);
clearTimeout(timeout);
timeout = setTimeout(functionCall, time);
};
};
And this is how to use it.
const debouncedIteration = debouce(() => {
console.log("Iteration"); // This will be called once every 1000 milliseconds.
}, 1000);
while (true) {
debouncedIteration();
}
You can avoid re-renders using this lifecycle method shouldComponentUpdate (as also mentioned by #fungusanthrax). Keep this inside your react component :
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
using isEqual from lodash here, make sure to include it.
This will only re-render your component when there's a change in props or state value.
To install lodash:
npm install -S lodash
and import isEqual in your component file :
import isEqual from 'lodash/isEqual';
I am trying to generate an array of UI controls as they're being rendered and the resulting state contains duplicate entries. Is there a good way to protect against this?
This code is fired each time a component is rendered. There are multiple tables with similar controls and I'm using this to get the maximum number of rows per control:
const newcontrols: LoadedCtrls = {
"itemCtrlType": controlType,
"rowsCount": numberRows,
};
if (this.state.loadedcontrols.length == 0) {
this.setState({
loadedcontrols: [...this.state.loadedcontrols, newcontrols]
})
}
for (var i = 0; i < this.state.loadedcontrols.length; i++) {
if (this.state.loadedcontrols[i].itemCtrlType == controlType) {
if (this.state.loadedcontrols[i].rowsCount < numberRows) {
this.setState({
loadedcontrols: [...this.state.loadedcontrols.slice(0, i), newcontrols, ...this.state.loadedcontrols.slice(i)]
})
}
}
else {
this.setState({
loadedcontrols: [...this.state.loadedcontrols, newcontrols]
})
}
}
The result in the console is the following:
[{"itemCtrlType":"map1","rowsCount":2},{"itemCtrlType":"map2","rowsCount":3},{"itemCtrlType":"location","rowsCount":5},{"itemCtrlType":"map2","rowsCount":3},{"itemCtrlType":"monitor","rowsCount":7},{"itemCtrlType":"monitor","rowsCount":7}]
I've attempted to use good practices with immutability but it still seems like setState is firing off with duplicates, and I know it is an asynchronous operation. So is there a way to prevent this?
EDIT: The function is triggered each time a component's fetch function outputs data, and the state is set in the component:
.then(data => {
this.setState({
results: data,
loading: false,
}, () => {
this.finishLoad(this.state.controlType, this.state.customerId, data.length);
});
});
The use of setState() can lead to very uncertain behavior; for two reasons:
setState() should not be used as a synchronous call (i.e., you rely on state being updated immediately).
Calling setState() as you are iterating over the state may not have the intended behavior (because of 1.).
You might consider a more general pattern:
let localState=this.state;
// Rely on and modify localState, as desired
this.setState(localState) // Set final state and trigger potential re-render
You may not need to specify the callback in your initial setState() call since state changes are batched and the next render is implicitly invoked.
The following read may be helpful setState() State Mutation Operation May Be Synchronous In ReactJS
I wonder if someone can explain the reason of this behavior:
If on a onChange event from an <input> element I have set to point to this method:
private PasswordChanged = (event: any) => {
this.setState((prevState: IWifiState, props: IWifiProps) => {
prevState.Password = event.target.value;
return prevState;
});
}
This throw me the following error:
Where line 27 is precisely the call to event.target.value on the pasted code.
If I change to code to be like that:
private PasswordChanged = (event: any) => {
const password = event.target.value;
this.setState((prevState: IWifiState, props: IWifiProps) => {
prevState.Password = password;
return prevState;
});
}
It just works as expected... Anyone can explain why?
Thanks!
React does something called Event Pooling.
What this essentially means is that, for performance considerations, they re-use events.
At the time when you call setState, internally the object might not be okay to re-use as it might behave in ways you wouldn't expect it to (properties get nulled out once the event has served it's purpose).
It is best to save off the reference in a variable for the value that you need, as you did, and use that instead.
Basically, you are accessing it asynchronously (inside the setState function) and it is advised against doing so.
There is a workaround, but I would also advise against it.
If you want to access the event properties in an asynchronous way, you should call event.persist() on the event, which will remove the synthetic event from the pool and allow references to the event to be retained by user code.