I am a beginner in React and I follow a tutorial in Udemy.I had a confusion about state.
When I am trying to update the state depending on the previous state, why the mentor says that we need to always use the second approach while both approaches seems logical to me.
This is my initialization
const [UserInput, setUserInput] = useState({
enteredTitle:'',
enteredDate:'',
enteredAmount:''
});
So here is the first approach.
const handleTitleChange = (event) =>{
setUserInput({
...UserInput,
enteredTitle:event.target.value
})
}
This is my second approach.
const handleTitleChange = (event) =>{
setUserInput((prevState) => {
return{
...prevState, enteredTitle:event.target.value}
});
Your mentor isn't right. Your first approach can well work in many situations, as you've probably noticed. But there are some in which you do need to use the second approach - namely, when the function, when it gets invoked, might close over an old value of the state. For example, if you have a button that, when clicked, will then change state after 2 seconds - the state may change in those 2 seconds, in which case the UserInput state that the function in the timeout closes over would be out of date.
// Don't do this
const handleClick = () => {
setTimeout(() => {
setState({ ...state, newProp: 'newVal' });
}, 2000);
};
Using the callback form instead - setState(state => or setUserInput((prevState) => - is necessary when you don't have a reliably up-to-date state variable already in scope.
Many prefer to use the simple state setter method - eg
setUserInput({
...UserInput,
enteredTitle:event.target.value
})
and avoid the callback version unless situation calls for the callback version. It's less code to write, read, and parse.
Related
note: I am aware of the useAbortableFetch hook. Trying to recreate a simple version of it.
I am trying to create a hook that returns a function that can make an abortable fetch request.
Idea being I want this hook to hold the state and update it when needed.
The update part is controlled by another competent on input change
What I am working on currently is
function useApiData(baseUrl){
const [state, setState] = use state({
data: null,
error: null,
loading: false
})
const controller = useRef(new AbortController)
const fetchData = searchTerm => {
if(state.loading){
controller.current.abort()
controller.current = new AbortController;
}
const signal = controller.signal;
setState(state => ({...state, loading: true})
fetch(url + searchTerm, {signal})
.then(res => res.json())
.then(data => {
setState(state => ({...state, data}))
return data
})
.catch(error => {
setState(state => ({...state, error}))
})
.finally(() => setState({...state, loading: false}))
}
const fetchCallback = useCallback(debounce(fetchData, 500), [])
return {...state, search: fetchCallback}
}
Usage
function App(){
const dataState = useApiData(url);
return ComponentWithInputElement {...dataState} />
}
function ComponentWithInputElement(props){
const [value, setValue] = useState('')
const onInput = ev => {
setValue(ev.target.value)
props.search(ev.tagert.value)
}
return (
<>
<input value={value} on input={onInput}>
{props.data?.length && <render datacomp>}
</>
)
}
This seems to fail to even send the first request.
Any way to make this pattern work?
Doing this in a useEffect would be very simple but I won't have access to the input value to have it as a dep
useEffect(()=>{
const controller = new AbortController();
const signal = controller.signal
fetch(url + value, {signal})
return () => controller.abort()
},[value])
Part of what you are trying to do does not feel "right". One of the things you are trying to do is have the state of the input value (like the form state) stored in the same hook. But those are not the same bits of state, as when the user types, it is (temporarily until its saved back to the server) different to the state fetched from the server. If you reuse the same state item for both, in the process of typing in the field, you lose the state fetched from the server.
You may think, "but I don't need it any more" -- but that often turns out to be a false abstraction later when new requirements come about that require it (like you need to display some static info as well as an editable form). In that sense, in the long term it would likely be less reusable.
It's a classic case of modelling an abstraction around a single use case -- which is a common pitfall.
You could add a new state item to the core hook to manage this form state, but then you have made it so you can only ever have the form state at the same level as the fetched data -- which may work in some cases, but be "overscoping" in others.
This is basically how all state-fetch libs like react query work -- Your fetched data is separate to the form data. And the form data is just initialised from the former as its initial value. But the input is bound to that "copy".
What you want is possible if you just returned setState from an additional state item in the core hook then passed down that setState to the child to be used as a change handler. You would then pass down the actual form string value from this new state from the parent to the child and bind that to the value prop of the input.
However, I'd encourage against it, as its an architectural flaw. You want to keep your local state, and just initialise it from the fetched state. What I suggested might be OK if you intend to use it only in this case, but your answer implies reuse. I guess I would need more info about how common this pattern is in your app.
As for abort -- you just need to return the controller from the hook so the consumer can access it (assuming you want to abort in the consumers?)
I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"
If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}
This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);
I'm trying to implement something like a Mobile Preview section where after the user do their things in the editor, the changes that they've made will be shown in the Preview section concurrently.
The issue that I'm facing now is the method that I'm using in Bulletin.js to retrieve the html content from the editor seems to be 1 step behind (as in I need to do some actions like clicking anywhere or to retrieve the last action made in the editor).
I want to make it so that the change is instant and not one step behind so that when user do things like changing font colour etc, it will be reflected to the preview section instantly.
Bulletin.js
const getContent = (htmlContentProp) => {
setHtmlContent(draftToHtml(htmlContentProp));
};
<RichTextEditor getContent={getContent} htmlContent={htmlContent} />
RichTextEditor.js
const handleEditorChange = (state) => {
setEditorState(state);
getContent(convertToRaw(editorState.getCurrentContent()));
};
Issue is here:
const handleEditorChange = (state) => {
setEditorState(state); // this is asynchronous
// so this will most likely be old value
getContent(convertToRaw(editorState.getCurrentContent()));
};
You have 2 easy options to work around this
One is to not use hook here at all, you can consume your "state" directly
const handleEditorChange = (state) => {
getContent(convertToRaw(state.getCurrentContent()));
};
Other option is to use useEffect which is more "correct" option if you for some reason need the hooks here
const handleEditorChange = (state) => {
setEditorState(state); // this is asynchronous
};
useEffect(() => {
getContent(convertToRaw(editorState.getCurrentContent()));
}, [editorState]); // this effect will trigger once the editorState actually changes value
When this line getContent(convertToRaw(editorState.getCurrentContent())) runs in the handleEditorChange function, the editorState is not yet updated with the latest value. Since React state updates are async
You can either use the state parameter in the handleEditorChange to get the latest data like below
const handleEditorChange = (state) => {
setEditorState(state);
getContent(convertToRaw(state.getCurrentContent()));
};
or use a useEffect to update state in parent based on changes in the child state.
I am trying to change the state programmatically while using the useState hook.
I defined it this way:
const [data, setData] = React.useState({
text: "Hello StackOverflow!",
someNumber: 2
});
For testing purposes, I created an interval which increases someNumber every second.
setInterval(() => {
setData({ ...data, someNumber: data.someNumber + 1 });
}, 1000);
When I now mount the component, the text starts flashing very fast with different numbers. I guess the Interval is started multiple times but I don't know why. It's my first project using the new hooks.
A full example of my code can be found on code sandbox snippet
And in case the link goes down, here's the component:
import React from "react";
import TextField from "#material-ui/core/TextField";
export default function StateTextFields() {
const [data, setData] = React.useState({
text: "Hello StackOverflow!",
someNumber: 2
});
setTimeout(() => {
setData({ ...data, text: "How are you?" });
}, 1000);
setInterval(() => {
setData({ ...data, someNumber: data.someNumber + 1 });
}, 1000);
return (
<TextField
id="standard-name"
label="Name"
value={data.text + " " + data.someNumber}
margin="normal"
/>
);
}
Move your setInterval inside a useEffect. This will stop it from creating a new interval every render.
React.useEffect(() => {
setInterval(() => {
setData(prevState => ({ ...prevState, someNumber: prevState.someNumber + 1 }));
}, 1000);
setTimeout(...)
},[])
This will make it so that these functions only run one time when the component mounts.
Unconditionally calling a function that modifies state inside a functional component is like doing the same in the render of a class component. It will modify state, which causes a re-render, which will modify state, and so on infinitely.
You need to use the prevState callback to update. This is because useEffect is not listening to changes made to data like useEffect(() => {}, [data]) making data go stale. But we also don't want to change it to listen to changes, because that would cause your interval to be created infinitely again. So we use prevState which always uses the most recent copy of state.
You are setting setTimeout and setInterval on each component render call.
Your Timeouts/Intervals will get fired on every re-render. You have to put it in a useEffect which replaced lifecycle hooks like componentDidMount().
Example:
useEffect(()=>{
const myInterval = setInterval(() => {
setData({ ...data, someNumber: data.someNumber + 1 })
}, 1000);
return ()=>{myInterval.clearInterval()} //cleanup
}, []) //The empty array means "only do this once, after the first render"
There are multiple problems here.
You are setting multiple timeouts/intervals that deal with the same data. They are updating the same values over different periods of time because of the asynchronicity. For example: The timeout changes data at almost the same time as the interval. Which results in the timeout calculating 2 + 1, meanwhile the interval thinks data.someNumber is still 2 instead of 3.
When not using hooks like React.useEffect react will recreate the methods each time it rerenders the component, a.k.a. every time state changes, a.k.a. every time data changes.
As BrianThompson pointed out the following solution fixes these problems. I added some comments to explain why.
React.useEffect(() => {
setInterval(() => {
// using prevState is essential here because it negates the need for 'data' as
// that would force you to recreate the hook every time 'data' changes
setData(prevState => ({ ...prevState, someNumber: prevState.someNumber + 1 }));
}, 1000);
setTimeout(...)
},[])
I've recently started to try to learn React hooks, but for the life of me I can't figure out some things, like multiple state management, I've found a few example but nothing seems to be in a specific pattern. I'm not even sure how you're supposed to define your states if there's more than 2 and you want to change them individually, should it be like this:
const [slides, setSlides] = useState([])
const [currentSlide, setCurrentSlide] = useState(0)
const [tagName, setTagName] = useState([])
Or like this:
const [states, setStates] = useState({
slides: [],
currentSlide: 0,
tagName: []
})
And if both or the second one is viable (honestly I would prefer the second one since its less repetitive in calling useState and closer to the standard state meta) how could one go about changing states in such a example? I couldn't really find anything on this, so what I tried was this:
useEffect(() => {
Object.entries(Slides).forEach(slide => {
setStates(prevState => ({ slides: [...prevState.slides, slide[1]] }))
})
}, [])
And I messed around with it a bunch trying to get some decent results but I couldn't get it to work properly.
Any ideas of what I'm doing wrong? And on which one of these to methods of best practice?
Thanks!
In terms of updating state, first template is much easy and simple, because each stateUpdate function will be responsible for updating single value, that can be obj/array/int/bool/string. Another thing is, to access each value of the state (it will be an object), you need to write states.slides, states.tagName etc.
With first case, state update will be simply:
// only value in case of int/string/bool
updateState({ [key]: value }) or updateState(value);
But in second case, state will be an object with multiple keys, so to update any single value you need to copy the object, then pass the updated key-value. Like this:
updateState({
...obj,
[key]: newValue
})
To update the slides array:
updateState(prevState => ({
...prevState,
slides: newArray
}))
Handling complex state update:
Use useReducer to handle the complex state update, write a separate reducer function with action types and for each action type so the calculation and return the new state.
For Example:
const initialState = { slides: [], tagName: [], currentSlide: 0 };
function reducer(state, action) {
switch (action.type) {
case 'SLIDES':
return { ... };
case 'TAGNAME':
return { ... };
case 'CURRENT_SLIDE':
return { ... }
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
....
}
It is true that the useState hook can become quite verbose when dealing with complex state object.
In this case, you can also consider using the useReducer hook.
I would recommend the reading of this article about useState vs useReducer.