React Component should mount previous state before fetching new data - reactjs

DataContext.js
export const DataContext = createContext()
const DataContext = ({ children }) => {
const [userValue, setUserValue] = useState()
const [user, setUser] = useState()
const fetch = useCallback(async () => {
const response = await axios(Url)
setBalance(response.data)
}
}, [])
usePrevious.js
import { useEffect, useRef } from 'react'
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
export default usePrevious
UsersData Component
import { DataContext } from '../context/DataContext'
import usePrevious from './usePrevious'
const UsersValue = () => {
const { userValue} = useContext(DataContext)
const prevData = usePrevious(useValue)
useEffect(() => {
console.log('prevData ', prevData)
console.log('userValue', userValue)
}, [useValue, prevData])
return (....)
Following the React documentation using usePrevious example.
When I render the component for the first time it takes few sec to load the data(async), this is fine, but if I route away to another link and come back, I still get the component loading the data and I want to fetch the previous state if the data has not changed
I was specting the console.log for prevData to show the previous state(when linked back to component) while component is fetching, but is just showing the same for userValue, both have the same state undefined, than data comes is available
How can I render a memorized state for the UsersData Component,to avoid loading all the time when component is mounting, I start looking into react.memo, is this the direction I need to take?
If so how can I implement React.memo in this case, thx

I don't think usePrevious will help you if you are unmounting and re-mounting the component, the ref will be recycled. You could pass down the fetch function from the context, and call it in the child component when it mounts. This fetch would run in the context, and if there is a difference between the new and data stored in the context, update the userValue using setUserValue in the context. This change would then be passed down to the child after the state has been updated. If they are the same, do not call setUserValue and no update will occur.
The problem here is if there is a new value, the child component won't know until the fetch has finished. You may be displaying old data off the bat, but if you don't tell the user you are fetching, the child component may update unexpectedly when the fetch is finished and the context is updated.
Caching is difficult, you may just want to always provide a fresh value.
If you decide to go this route, you will need to check if the data you fetched has changed. If the data is an object and doesn't have functions added to it, a quick way is to use JSON.stringify on both objects and compare the stringified values.

Related

React Hooks, how to prevent unnecessary rerendering

I have a hook that gets data from another hook
const useIsAdult = () => {
const data = useFormData();
return data.age > 18;
}
This hook returns true or false only, however the useFormData is constantly being updated. Everytime useFormData reruns, it will rerender my component. I only want my component to rerender if the result of useIsAdult changes.
I know this can obviously be solved by implementing some logic via react-redux and using their useSelector to prevent rendering, but I'm looking for a more basic solution if there is any.
Before you say useMemo, please read question carefully. The return value is a boolean, memo here doesnt make any sense.
Even with returned useMemo in useIsAdult parent component will be rerendered. This problem is reason why I rarely create custom hooks with other hooks dependency. It will be rerender hell.
There I tried to show that useMemo doesnt work. And using useMemo for components its wrong way. For components we have React.memo.
const MyComponent = memo(({ isAdult }: { isAdult: boolean }) => {
console.log("renderMy");
return <h1>Hello CodeSandbox</h1>;
});
And memo will help to prevent rendering of MyComponent. But parent component still render.
https://codesandbox.io/s/interesting-lumiere-xbt4gg?file=/src/App.tsx
If you can modify useFormData maybe useRef can help. You can store data in useRef but it doesnt trigger render.
Something like this but in useFormData:
onst useIsAdult = () => {
const data = useFormData();
const prevIsAdultRef = useRef();
const isAdult = useMemo(() => data.age > 18, [data]);
if (prevIsAdultRef.current === isAdult) {
return prevIsAdultRef.current;
}
prevIsAdultRef.current = isAdult;
return isAdult;
};
I dont know how useFormData works, then you can try it self with useRef.
And if useFormData gets data very often and you cant manage this, you can use debounce or throttle to reduce number of updates
Memoize hook
const useIsAdult = () => {
const data = useFormData();
return useMemo(() => data.age > 18, [data.age]);
}
Here, useMemo will let you cache the calculation or multiple renderes, by "remembering" previous computation. So, if the dependency (data.age) doesn't change then it will use simply reuse the last value it returned, the cached one.
Memoize component expression
useMemo can also be used to memoize component expressions like this: -
const renderAge = useMemo(() => <MyAge age={data.age} />, [data.age]);
return {renderAge}
Here, MyAge will only re-render if the value of data.age changes.

Why is the updated state variable not shown in the browser console?

I understand that the method returned by useState is asynchronous, however, when I run the this code, I am delaying the console.log by upto 5 seconds, but it still logs the previous value and not the updated value of the state variable. The updated value would be 2, but it still logs 1. In the react developer tools however, I can see the state changing as I press the button, though I am curious to know why after even such a delay the console prints an obsolete value? This is not the case with class components and setState but with function components and useState.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
setTimeout(() => {
console.log(variable);
}, 2000);
};
return <button onClick={handleClick}>Button</button>;
}
export default App;
In your code your setTimeout is getting the variable value from the closure at the time it was invoked and the callback function to the setTimeout was created. Check this GitHub issue for the detailed explanation.
In the same issue, they talk about utilizing useRef to do what you are attempting. This article by Dan Abramov packages this into a convenient useInterval hook.
State updates are asynchronous. That means, that in order to view the new value, you need to log It on the next render using useEffect and adding it to the dependencies array:
In this example, give a look at the order the logs appear:
First, you will have the current one, and once triggered, you will have the new value, and then it will become the 'old value' until triggered again.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => { console.log(`new state rolled: ${counter}`);
}, [counter]);
console.log(`Before rolling new State value: ${counter}`);
const handleClick = () => setCounter(counter++)
return <button onClick={handleClick}>Button</button>;
}
export default App;
Another technic to console.log a value afterward a state change is to attach a callback to the setState:
setCounter(counter++, ()=> console.log(counter));
I hope it helps.
A state take some time to update. The proper way to log state when it updates, is to use the useEffect hook.
setTimeout attaches the timer and wait for that time, but it will keep the value of variable from the beginning of the timer, witch is 1
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
};
useEffect(() => {
console.log(variable);
}, [variable]);
return <button onClick={handleClick}>Button</button>;
}
export default App;
This is not the case with class components and setState but with
function components and useState
In class components, React keep the state in this.state & then call the Component.render() method whenever its need to update due to a setState or prop change.
Its something like this,
// pseudocode ( Somewhere in React code )
const app = MyClassComponent();
app.render();
// user invoke a callback which trigger a setState,
app.setState(10);
// Then React will replace & call render(),
this.state = 10;
app.render();
Even though you cannot do this.state = 'whatever new value', React does that internally with class components to save the latest state value. Then react can call the render() method and render method will receive the latest state value from this.state
So, if you use a setTimeout in a class component,
setTimeout(() => {
console.log(this.state) // this render the latest value because React replace the value of `this.state` with latest one
}, 2000)
However in functional component, the behaviour is little bit different, Every time when component need to re render, React will call the component again, And you can think the functional components are like the render() method of class components.
// pseudocode ( Somewhere in React code )
// initial render
const app = MyFuctionalComponent();
// state update trigger and need to update. React will call your component again to build the new element tree.
const app2 = MyFunctionalComponent();
The variable value in app is 1 & variable value in app2 is 2.
Note: variable is just a classic variable which returned by a function that hooked to the component ( The value save to the variable is the value return by the hook when the component was rendering so it is not like this.state i.e its hold the value which was there when the component is rendering but not the latest value )
Therefore according to the Clouser, at the time your setTimeout callback invoke ( Which was called from app ) it should log 1.
How you can log the latest value ?
you can use useEffect which getting invoke once a render phase of a component is finished. Since the render phase is completed ( that mean the local state variables holds the new state values ) & variable changed your console log will log the current value.
useEffect(() => {
console.log(variable);
}, [variable])
If you need the behaviour you have in class components, you can try useRef hook. useRef is an object which holds the latest value just like this.state but notice that updating the value of useRef doesn't trigger a state update.
const ref = useRef(0);
const handleClick = () => {
setVariable(2); // still need to setVariable to trigger state update
ref.current = 2 // track the latest state value in ref as well.
setTimeout(() => {
console.log(ref.current); // you will log the latest value
}, 2000);
};

When does useEffect call when I use it with useContext?

I call useEffect inside useContext and I want to know when this useEffect is called.
[settingContext.tsx]
// create context object
export const ColorContext = createContext<ColorContextType>(null);
export const ProductsProvider = (props) => {
const { query } = useRouter();
const [data, setData] = useState<ColorContextType>(null);
useEffect(() => {
async function fetchAPI() {
const res = await fetch(`${env.API_URL_FOR_CLIENT}/page_settings/top`);
const posts = await res.json();
setData(posts);
}
fetchAPI();
}, []);
return <ColorContext.Provider value={data}>{props.children}</ColorContext.Provider>;
};
export const useColorContext = () => {
const colors = useContext(ColorContext);
let themeColor: string = '';
let titleColor: string = '';
if (colors !== null) {
const colorData = colors.response.result_list[3].value;
themeColor = JSON.parse(colorData).theme_color;
titleColor = JSON.parse(colorData).title_color;
}
return { themeColor, titleColor };
};
[_app.tsx]
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<LayoutInitial>
<ProductsProvider>
<Component {...pageProps} />
</ProductsProvider>
</LayoutInitial>
);
}
I use useColorContext on multiple components.
It seems like useEffect is only called on '/' page, which is fine but I'm curious that useEffect should be called every time pages are rendered but it seems that it doesn't.
is this because I use useContext?
The useEffect call is done in the ProductsProvider component which appears to only be rendered once, on page load/refresh. This is because components generally only re-render when a state they subscribe to is changed. If the useEffect were called directly within the <Component> component, it would be called every time the component is mounted (not in re-renders). useEffect is only called multiple times after mounting if one of its dependencies changes, which in your case, there are none.
For example: this sandbox
It's composed of the App component, containing a Router to a homepage, a ComponentA route, and a ComponentB route. When each component mounts, its useEffect is called, creating an alert box. You'll only see the App useEffect alert once per page refresh.
ComponentA will have its useEffect called when the component mounts (every time you hit the /a route from a different route), and when the state in the component changes, since it's in the useEffect dependency array.
ComponentB will only have its useEffect called when the component mounts, and not when its state changes, because the state isn't included in the useEffect dependency array.
EDIT: To clarify, your useColorContext hook is not actually part of the ProductsProvider component, so the useEffect call is not "inherited" by any components that call the hook. Also, keep in mind when experimenting that using Strict Mode will cause components to render twice, allowing react to gather information on the first render, and display it on the second render.

What is the best way to update my react state when data in database changes?

I am workin on a project with React and I need to update react state with react hook when new data is inserted to my database. I am using contextAPI but as far as I am concerned, the way my components are structured I can't update that state with the data I got back from my database. Here is the more detail with my code:
I have a component in which data exists with useState
const [questions, setQuestions] = useState([]);
useEffect(() => {
(async () => {
const resp = await axios.get(
`${process.env.REACT_APP_BACKEND_URL}/product/${productId}/question`
);
setQuestions(resp.data);
})();
}, []);
And I have another component called PostQuestion which sends post request to my backend and insert new question to my database. They are completely unaware of each other's existence. we can pass down the props to the children and nested children by using contextAPI. but PostQuestion component is not the child of that component where all the questions data exist. That's how I understand contextAPI. But both of them are children of another component. But I don't want to place my
const [questions, setQuestions] = useState([]);
in that component which is the parent of those two components. What can I do to update that questions state inside my PostQuestion component?
Here is my PostQuestion component code
const postQuestionHandler = async (e) => {
e.preventDefault();
try {
const resp = await axios({
url: `${process.env.REACT_APP_BACKEND_URL}/product/${productId}/question`,
method: "POST",
data: {
question: questionInput.question.value,
userId,
},
});
if (resp.status === 200) {
setShowForm(false);
}
} catch (err) {
alert(err);
}
};
return <SecondaryBtn disabled={questionInput.question.error}>
Submit Your Question
</SecondaryBtn>
Summary
I have 2 components which don't have parent to child relationship. I want to update the state in component 1 by calling setState when I got back new data in component 2. I don't want to have state in those two components parent because it is already cluttered enough.
If I use Redux, there will be no such problem. Perhaps there is a way to do it in contextAPI too.
I think Context API is the best way.
You can create a context with React.createContext, and then a component that will be encapsulating your context provider. So, a PostsContext and then a PostsProvider that would store the posts state and pass both the state and the setState to the PostsContext.Provider.
Then, to make it simpler, create a usePosts hook and on those children that need to consume the context, simply do const [posts, setPosts] = usePosts().
Alright, so you'd have:
A Context;
A Provider component of your own, that would use the Context;
A hook that would consume the Context with useContext;
Just to make it clearer:
A context:
const PostsContext = React.createContext();
A provider:
function PostsProvider(props) {
const [posts, setPosts] = React.useState([]);
return <PostsContext.Provider value={{ posts, setPosts }} {...props} />
}
A hook:
function usePosts() {
const context = React.useContext(PostsContext);
function postQuestionHandler(newPost) {
// ...
context.setPosts((posts) => [...posts, post]);
}
return [context.posts, postQuestionHandler]
}
And then you can use the PostsProvider to encapsulate the children, and those components can access the context using the usePosts hook.
That would be a complete cenario, you can divide the logic some other ways, like not creating a custom hook nor a custom Provider. I, personally, would prefer to just lift the state in this case, but as you said your parent component is already handling too much, perhaps that's a good option.
Multiple solutions:
You could poll in an interval.
Store the state in a parent component (at some point they have so share one) and pass it down.
Utilize a global state management tool like redux, or the context API of react. I would recommend the context API as it's built in.

Mixing Redux with useEffect Hook

I read that this is theoretically OK. In my small use case, I'm running into an issue, where mixing those technologies leads to double re-rendering.
First, when redux dispatch is executed and some components use a prop via useSelector. Then, after the functional component is already re-rendered, a useEffect hook is being applied which updates the state of some property. This update re-triggers the render again.
E.g. the below console log prints out twice in my case.
Question: should I remove the useEffect and useState and integrate it into redux' store?
import {useSelector} from "react-redux";
import React from "react";
const Dashboard = (props) => {
const selFilters = useSelector((state) => state.filter.selectedDashboardFilters);
const [columns, setColumns] = React.useState([]);
React.useEffect(() => {
let newColumns = getColumns();
setColumns(newColumns)
}, [selFilters]
)
console.log("RENDER")
return (
<h1>{columns.length}</h1>
)
}
If columns needs to be recomputed whenever selFilters changes, you almost certainly shouldn't be recomputing it within your component. If columns is computed from selFilters, then you likely don't need to store it as state at all. Instead, you could use reselect to create a getColumns() selector that derives the columns from the state whenever the relevant state changes. For example:
const getColumns = createSelector(
state => state.filter.selectedDashboardFilters,
selFilters => {
// Compute `columns` here
// ...
return columns
}
)

Resources