I am learning React and having difficulty in understanding the flow of code that is happening.
Here is my code:It is a functional React component
function App() {
const [x, setx] = useState(1);
const [x1, setx1] = useState(1);
const [x2, setx2] = useState(1);
const [x3, setx3] = useState(1);
const [x4, setx4] = useState(1);
const [x5, setx5] = useState(1);
console.log("out3");
const bclick = () => {
setx1(x1 + 1);
setx2(x2 + 1);
setx3(x3 + 1);
setx4(x4 + 1);
setx5(x5 + 1);
setx(x + 1);
console.log("out1");
bclick2();
};
const bclick2 = () => {
console.log("out2");
};
console.log("out4");
return (
<div className="App">
{console.log("in")}
<button onClick={bclick} />
</div>
);
}
output of console.log() after clicking on button:
out1
out2
out3
out4
in
Q> Upon clicking on button multiple different setStates are executed. Will they re-evaluate the component or the function chain(bclick and bclick2) complete executing and then App component is re-evaluated.
Based on my output I realise that function chain is executed first.
So is this how setState works?
Will flow of code complete first (irrespective of number of functions) and then functional component re-evaluated?
This has to do with React batching setState calls to optmize the number of renders. Usually you don't have to worry about that.
The setState call is async. React will decide when and how to apply multiple setState calls.
The handlers will always finish running before React re-renders. That's why you are seeing the bclick2() call running before any re-renders.
I feel that React will always go for batching multiple setState calls in a single re-render. But you can see that if you wrap multiple setState calls in setTimeout, React will re-render multiple times, because there's no way of it to know how long those timeouts will take to complete. You might be calling an API, for example.
function App() {
console.log('Rendering App...');
const [x, setx] = React.useState(1);
const [x1, setx1] = React.useState(1);
const [x2, setx2] = React.useState(1);
const [x3, setx3] = React.useState(1);
const [x4, setx4] = React.useState(1);
const [x5, setx5] = React.useState(1);
const bclick = () => {
console.clear();
console.log("From bclick (batched: single render)");
setx1(x1 + 1);
setx2(x2 + 1);
setx3(x3 + 1);
setx4(x4 + 1);
setx5(x5 + 1);
setx(x + 1);
console.log("Calling bclick2");
bclick2();
};
const bclick2 = () => {
console.log("From bclick2");
};
const notBatched = () => {
console.clear();
console.log('From notBatched (multiple renders)');
setTimeout(() => setx1((prevState) => prevState+1),0);
setTimeout(() => setx1((prevState) => prevState+1),0);
setTimeout(() => setx1((prevState) => prevState+1),0);
};
return (
<div className="App">
<button onClick={bclick}>Click (will batch)</button>
<button onClick={notBatched}>Click (will not batch)</button>
</div>
);
}
ReactDOM.render(<App/>,document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
For example, if you are calling an API from a useEffect:
useEffect(() => {
setIsLoading(true); // 1
const data = await fetchAPI();
setData(data); // 2
setIsLoading(false); // 3
},[]);
In this case React wil run #1, and then, when the API call completes, it will run #2 and #3 separately (not batched). Not sure why it chooses to do it separately, because it would be safe to run them together, but I'm sure React has its own reasons for that. Probably the whole block that has been timeout'd because of the API call is flagged to shouldNotBatch somehow. I don't actually know what is the internal logic they use for this.
const fetchAPI = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('DATA'),1500);
});
}
const App = () => {
console.log('Rendering App...');
const [isLoading,setIsLoading] = React.useState(false);
const [data,setData] = React.useState(null);
// I changed the `await` to `then` because SO snippets don't allow `await` in this case
React.useEffect(() => {
console.log('Running useEffect...');
setIsLoading(true); // 1
fetchAPI().then((data) => {
setData(data); // 2
setIsLoading(false); // 3
});;
},[]);
return(
<div>
<div>isLoading:{JSON.stringify(isLoading)}</div>
<div>data:{JSON.stringify(data)}</div>
</div>
);
};
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
React v18 (apr-2022)
Apparently React v18 batches call from async handlers as well.
https://vogue.globo.com/celebridade/noticia/2022/04/bbb-portugal-bruna-gomes-e-pedida-em-namoro-por-bernardo-sousa.html
Related
I've got an app with 2 values that user can change stored as state. When they're changed I do some processing in useEffect and also store output of this processing in state and render it.
At this point everything is working perfectly fine, but this processing takes some time and I want to show some loading indicator. I want it to show after button click.
This is simplified version of my app and dumb implementation of this loading indicator - I know why this doesn't work, I just don't know how to write it correctly so I made this just to show what I'm trying to do. Here is the code:
function App() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const [output, setOutput] = useState(0);
const [isLoading, setIsLoading] = useState(false); // setting true here won't work,
// because I want to show loading indicator after user change value1 or value2
// and not on inital load
useEffect(() => {
if (!value1 && !value2) {
return;
}
setIsLoading(true);
for (let i = 0; i < 1999999999; i++) {} // some long operations
setOutput(value1 + value2);
setIsLoading(false);
}, [value1, value2]);
return (
<div>
<button onClick={() => setValue1(value1 + 1)}>increment value1</button>
<button onClick={() => setValue2(value2 + 1)}>increment value2</button>
<div>
{value1} + {value2} = {isLoading ? 'please wait...' : output}
</div>
</div>
);
}
You can start the value of isLoading equals true, so, the page will start on loading.
function App() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const [output, setOutput] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
for (let i = 0; i < 1999999999; i++) {} // some long operations
setOutput(value1 + value2);
setIsLoading(false);
}, [value1, value2]);
return (
<div>
<button onClick={() => setValue1(value1 + 1)}>increment value1</button>
<button onClick={() => setValue2(value2 + 1)}>increment value2</button>
<div>
{value1} + {value2} = {isLoading ? 'please wait...' : output}
</div>
</div>
);
}
Try this piece of code:
<div>
<button
onClick={() => {
setIsLoading(true);
setValue1(value1 + 1);
}}
>
increment value1
</button>
<button
onClick={() => {
setIsLoading(true);
setValue2(value2 + 1);
}}
>
increment value2
</button>
Complete code is here: https://codesandbox.io/s/relaxed-hill-jy1bfm?file=/src/App.js:689-1018
The problem is that it is asynchronous. There may be many different approaches to the solution, but for your simple example I have simple solution:
export default function App() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const [output, setOutput] = useState(0);
const isLoading = value1 + value2 !== output;
useEffect(() => {
if (!value1 && !value2) {
return;
}
for (let i = 0; i < 1000; i++) {
console.log(1);
} // some long operations
setOutput(value1 + value2);
}, [value1, value2]);
return (
<div>
<button onClick={() => setValue1(value1 + 1)}>increment value1</button>
<button onClick={() => setValue2(value2 + 1)}>increment value2</button>
<div>
{value1} + {value2} = {isLoading ? "please wait..." : output}
</div>
</div>
);
}
The problem is that between setting isLoading to true and setting it back to false (after the calculation-heavy operation) no rendering happened.
Several approaches now come to my mind; and I'm not sure which one actually works as expected and (from those that do) which one I would pick; so I just share what's on my mind:
approach 1 (adding it to the task queue of the main thread): only setting the loading flag synchronously (and thus returning from user code and handing the control flow back to the current render-run of react), and deliberately triggering the calculation in an asynchronous way
useEffect(() => {
if (!value1 && !value2) return;
setIsLoading(true);
setTimeout(() => {
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);
approach 2 (adding it to the microtask queue of the main thread): turn it into micro tasks (a.k.a. Promises): when your useEffect just creates and starts a promise object and then "forgets" about it, the control flow is handed back to react. when a promise resolves and changes your component state react will do a re-render. But I suspect this might not bring any change, because afaik the microtask queue runs on the main thread and will run until empty, before react can schedule the re-rendering task. Disclaimer: promises are not my strong suit and I might have fudged that up here.
useEffect(() => {
if (!value1 && !value2) return;
setIsLoading(true);
Promise.resolve().then(() => {
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);
approach 3 (using flushSync): ensuring the UI is rendered before your next statement. you are not allowed to call flushSync inside useEffect, therefore you need to place the call inside a new task or microtask
useEffect(() => {
if (!value1 && !value2) return;
Promise.resolve()
.then(() => flushSync(() => setIsLoading(true)))
.then(() => {
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);
or
useEffect(() => {
if (!value1 && !value2) return;
setTimeout(() => {
flushSync(() => setIsLoading(true));
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);
OnClick I want to call a function that sets my loading state to true then do a for loop which will take more than one second and on the last loop set the loading state back to false but in my code the loading state doesn't change as expected. What do I have to fix?
import { useState } from "react"
const Test = () => {
const [loading, setLoading] = useState(false)
const someFunction = () => {
setLoading(true)
const someVeryBigArray = [...]
for (let i = 0; i < someVeryBigArray.length; i++) {
if (i === someVeryBigArray.length - 1) {
setLoading(false)
}
}
}
return (
<button onClick={someFunction} className={`${loading && "text-red-500"}`}>
test
</button>
)
}
export default Test
You need to give the browser time to re-render. If you have a huge blocking loop, React won't be yielding control back to the browser so that it can repaint (or even to itself so that the component can run again with the new state).
While one approach would be to run the expensive function in an effect hook, after the new loading state has been rendered:
const Test = () => {
const [running, setRunning] = useState(false)
useEffect(() => {
if (!running) return;
const someVeryBigArray = [...]
for (let i = 0; i < someVeryBigArray.length; i++) {
// ...
}
setRunning(false);
}, [running]);
return (
<button onClick={() => setRunning(true)} className={running && "text-red-500"}>
test
</button>
)
}
A better approach would be to offload the expensive code to either the server, or to a web worker that runs on a separate thread, so as not to interfere with the UI view that React's presenting.
To be honest if in any case your loop is taking 1 second to run, then this will cost into you app's performance. And this is not the best way to do as well.
The better way would be, If your really want to replicate the delay in you app then you should use setTimeout() using which you delay some action. sharing a code snippet might help you.
JSX
import { useEffect, useState } from "react";
const Test = () => {
const [loading, setLoading] = useState(false);
let timevar = null;
const someFunction = () => {
setLoading(true);
timevar = setTimeout(() => {
setLoading(false); //this will run after 1 second
}, 1000); //1000 ms = 1 second
};
useEffect(() => {
return () => {
//clear time out if in case component demounts during the 1 second
clearTimeout(timevar);
};
});
return (
<button onClick={someFunction} className={`${loading && "text-red-500"}`}>
test
</button>
);
};
export default Test;
I have a function that filters through some state and renders out the result for a search request.
const handleSearch = (value: string) => {
const searchResultData = users.filter((userId) => user.id.startsWith(value));
setSearchResult(searchResultData);
};
I am trying to work with lodash.throttle library to cause a delay before the request is sent. So we don't have a request go out every time a user types.
const handleSearch = useCallback(throttle((value: string) => {
const searchResultData = users.filter((userId) => user.id.startsWith(value));
setSearchResult(searchResultData);
}, 2500), []);
This works in delaying input as expected but for some reason, the user.filter method doesn't run, and so the state isn't updated with the search result. I believe the problem might be from the useCallback hook, but the throttle function is dependent on it to run. Any ideas on how I can work around this problem?
If your throttled/debounced handler uses props or state, like this:
const { fetcherFunctionFromProps } = props;
const eventHandler = async () => {
const resp = await fetcherFunctionFromProps();
};
const debouncedEventHandler = useMemo(
() => throttle(eventHandler, 300)
), [fetcherFunctionFromProps]);
And it doesn't work,
you can refactor it to the following:
const { fetcherFunctionFromProps } = props;
const eventHandler = async (fetcher) => {
const resp = await fetcher();
};
const debouncedEventHandler = useMemo(() => throttle(eventHandler, 300), []);
...
<Component onClick={() => debouncedEventHandler(fetcherFunctionFromProps)}>
When state is in a hook it can become stale and leak memory:
function App() {
const [greeting, setGreeting] = useState("hello");
const cb = useCallback(() => {
alert("greeting is " + greeting);
}, []);
return (
<div className="App">
<button onClick={() => cb()}>Click me</button>
<p>
Click the button above, and now update the greeting by clicking the one
below:
</p>
<button onClick={() => setGreeting("bye")}>
Update greeting
</button>
<p>Greeting is: {greeting}</p>
<p>
Now click the first button again and see that the callback still has the
old state.
</p>
</div>
);
}
Demo: https://codesandbox.io/s/react-hook-stale-datamem-leak-demo-9pchk
The problem with that is that we will run into infinite loops in a typical scenario to fetch some data if we follow Facebook's advice to list all dependencies always, as well as ensure we don't have stale data or memory leaks (as the example showed above):
const [state, setState] = useState({
number: 0
});
const fetchRandomNumber = useCallback(async () => {
if (state.number !== 5) {
const res = await fetch('randomNumber');
setState(v => ({ ...v, number: res.number }));
}
}, [setState, state.number]);
useEffect(() => {
fetchRandomNumber();
}, [fetchRandomNumber]);
Since Facebook say we should list fetchRandomNumber as a dependency (react-hooks/exhaustive-deps ESLint rule) we have to use useCallback to maintain a reference, but it regenerates on every call since it both depends on state.number and also updates it.
This is a contrived example but I've run into this many times when fetching data. Is there a workaround for this or is Facebook wrong in this situation?
Use the functional form of the state setter:
const fetchData = useCallback(async () => {
const res = await fetch(`url?page=${page}`);
setData((data) => ([...data, ...res.data]));
setPage((page) => page + 1);
}, [setData, setPage]);
Now you don't need data and page as your deps
You can also use a ref to run the effect only on mount :
const mounted = useRef(false);
useEffect(() => {
if(!mounted.current) {
fetchSomething();
mounted.current = true;
}
return () => { mounted.current = false }
}, [fetchSomething]);
And
const fetchSomething = useCallback(async () => {
...
}, [setData, setPage, data, page]);
fetchSomething is not a dependency here. You don't want to retrigger the effect, you only cause it once when the component mounts. Thats what useEffect(() => ..., []) is for.
I'm trying to implement a data stream that has to use inner observables, where I use one from mergeMap, concatMap etc.
e.g.:
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
This works fine when logging into console.
But when I try to use it in React like below utilizing useEffect and useState hooks to update some text:
function App() {
const input$ = new Subject<string>();
const input$$ = input$.pipe(share());
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
// This works
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, [input$$]);
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
// This doesn't
return () => {
subscription.unsubscribe();
};
}, [output$$]);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
it starts acting weird/unpredictable (e.g.: sometimes the text is updated in the middle of typing, sometimes it doesn't update at all).
Things I have noticed:
If the inner observable completes immediately/is a promise that
resolves immediately, it works fine.
If we print to console instead of useEffect, it works fine.
I believe this has to do something with the inner workings of useEffect and how it captures and notices outside changes, but cannot get it working.
Any help is much appreciated.
Minimal reproduction of the case:
https://codesandbox.io/s/hooks-and-observables-1-7ygd8
I'm not quite sure what you're trying to achieve, but I found a number of problems which hopefully the following code fixes:
function App() {
// Create these observables only once.
const [input$] = useState(() => new Subject<string>());
const [input$$] = useState(() => input$.pipe(share()));
const [output$$] = useState(() => input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
));
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
// Create the subscription to input$$ on component mount, not on every render.
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, []);
// Create the subscription to output$$ on component mount, not on every render.
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
I had a similar task but the goal was to pipe and debounce the input test and execute ajax call.
The simple answer that you should init RxJS subject with arrow function in the react hook 'useState' in order to init subject once per init.
Then you should useEffect with empty array [] in order to create a pipe once on component init.
import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";
const App = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filterChangedSubject] = useState(() => {
// Arrow function is used to init Singleton Subject. (in a scope of a current component)
return new Subject<string>();
});
useEffect(() => {
// Effect that will be initialized once on a react component init.
// Define your pipe here.
const subscription = filterChangedSubject
.pipe(debounceTime(200))
.subscribe((filter) => {
if (!filter) {
setLoading(false);
setItems([]);
return;
}
ajax(`https://swapi.dev/api/people?search=${filter}`)
.pipe(
// current running ajax is canceled on filter change.
takeUntil(filterChangedSubject)
)
.subscribe(
(results) => {
// Set items will cause render:
setItems(results.response.results);
},
() => {
setLoading(false);
},
() => {
setLoading(false);
}
);
});
return () => {
// On Component destroy. notify takeUntil to unsubscribe from current running ajax request
filterChangedSubject.next("");
// unsubscribe filter change listener
subscription.unsubscribe();
};
}, []);
const onFilterChange = (e) => {
// Notify subject about the filter change
filterChangedSubject.next(e.target.value);
};
return (
<div>
Cards
{loading && <div>Loading...</div>}
<input onChange={onFilterChange}></input>
{items && items.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
);
};
export default App;