I have some problem with useEffect. When the counter changes it causes the whole table to be rerendered, but i dont pass timer as props in table. How i can prevent this behavior?
function App() {
const dispatch = useDispatch();
const data = useSelector(state => state.data);
const [error, setError] = useState("");
const [counter, setCounter] = useState();
useEffect(() => {
const fetchData = async (setError, setCounter) => {
try {
const response = await axios(url, token);
dispatch(getData(response.data.value));
setError("");
setCounter(180);
} catch(e) {
setError("Error!");
setCounter(180);
}}
fetchData(setError, setCounter);
const interval = setInterval(() => {
fetchData(setError, setCounter);
}, timeToReload * 1000);
const countInterval = setInterval(() =>
setCounter((prev) => prev - 1), 1000)
return () => {
clearInterval(interval);
clearInterval(countInterval);
}
},[dispatch])
const dataForTable = selectorData([...data], {name: sortArrow.columnName, order: sortArrow.sortOrder, type: sortArrow.type})
return (
<div className="App">
<div className="headerWrapper">
<div
className={error ? "LoadingStatus disconnect": "LoadingStatus connect"}>
{error && <div>{error}</div>}
{isFinite(counter) && <div>{"Reload " + counter + " sec"}</div> }
</div>
</div>
<Table specialCategory={specialCategory} data={dataForTable} sortArrow={sortArrow} setSortArrow={setSortArrow}/>
</div>
);
}
export default App;
I trued to useRef without useState, but nothing has changed. Maybe another props in Table component trigger the change?
Imptortant notice: only the body of the table is changed.
When you update the state (e.g. setCounter(...)) of the App component, it causes the entire component with all of it's child-components, including <Table/>, to be re-rendered.
You can either create a new component for everything except the table and put the states which are changing (error and counter) into that file, or memoize the Table component like this:
import { memo } from "react";
function Table(props) {
return (
// [...] Your Table component.
);
}
export default memo(Table);
And import it just as you do already. This will avoid re-rendering the table unless its props change.
See Here the Reasons of React Re-rendering:
If a Parent Component Re-renders, the Children re-render automatically. & because your counter state is on the Table Components parent, It will re-render every time Counter changes.
Related
I have a question on using useCallback on built-in/ non-custom components in React. Below I provided a simple example.
I wrapped "increment1" in useCallback so it would not be recreated and passed as a new function into MyOwnButton component. Thus, with React.memo, MyOwnButton component would only get rerendered when 'count1' is updated.
Here come my question. Should I wrap "increment2" in useCallback too? So it would not be passed as a new function into the tag (or simplified as "button2") and to prevent "button2" from unnecessary rerendering too?
import React, { memo, useCallback, useState } from 'react';
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const increment1 = useCallback(() => {
setCount1((prev) => prev + 1);
}, []);
const increment2 = () => setCount2((prev) => prev + 2);
return (
<>
<MyOwnButton count={count1} onClick={increment1} />
<button onClick={increment2}>button2:{count2}</button>
</>
);
}
const MyOwnButton = memo(({ count, onClick }: any) => {
console.log('MyOwnButton');
return <button onClick={onClick}>button1:{count}</button>;
});
I am pretty new to React Hooks and could not understand how to place a useState() function inside a child component, both in a child's body and in a child's inner function, like this:
function Parent () {
const [counter, setCounter] = React.useState();
return (
<Child counter={counter} setCounter={setCounter}/>
)
}
function Child (props) {
const counter = props.counter;
const setCounter = props.setCounter;
React.useEffect(() => {
setCounter(0);
})
const increment = () => {
setCounter(1);
}
return (
<div> {counter} <button onClick={increment}>Increase Count</button> </div>
)
}
My code keeps reupdating the state to 0. What's wrong?
Please check my fiddle.
Thanks!
You missed the second argument in useEffect.
useEffect will trigger on every render if the second argument is not there and it will reset the changed value back to 0 again
React.useEffect(() => {
setCounter(0);
},[])
Full code sample
function Parent () {
const [counter, setCounter] = React.useState();
return (
<Child counter={counter} setCounter={setCounter}/>
)
}
function Child (props) {
const counter = props.counter;
const setCounter = props.setCounter;
React.useEffect(() => {
setCounter(0);
}, [])
const increment = () => {
setCounter(counter + 1);
}
return (
<div> {counter} <button onClick={increment}>Increase Count</button> </div>
)
}
Your code works: https://codesandbox.io/s/billowing-bird-p9xzp?file=/src/App.js
But there are several things to consider here:
1- you can initialize the state to 0 in const [counter, setCounter] = React.useState(0);
2- You dont need useEffect in child component
Your useEffect hook is missing a dependency array, so it is being called every time the child component renders. This means every time you click increment, the state gets set to 1, but then your useEffect immediately fires again and resets the count to 0. Fix this by placing an empty dependency array at the end of your useEffect (this means it will only fire once when the component is first mounted):
React.useEffect(() => {
setCounter(0);
}, [])
Also if you are want your button to actually increment the count by one every time you have to setCounter to 1 + the previous state, otherwise the counter will just stay at 1 every time it is clicked:
const increment = () => {
setCounter(prevCounter => prevCounter + 1);
}
I want to find a way to avoid unnecessary re-renders when using useContext. I've created a toy sandbox, which you can find here: https://codesandbox.io/s/eager-mclean-oupkx to demonstrate the problem. If you open the dev console, you'll see the console.log happening every second.
In this example, I have a context that simply increments a counter every 1 second. I then have a component that consumes it. I don't want every tick of the counter to cause a re-render on the component. For example, I may just want to update my component every 10 ticks, or I may want some other component that updates with every tick. I tried creating a hook to wrap useContext, and then return a static value, hoping that would prevent the re-renders, to no avail.
The best way I can think of to overcome this issue is to do something like wrap my Component in a HOC, which consumes count via useContext, and then passes the value into Component as a prop. This seems like a somewhat roundabout way of accomplishing something that should be simple.
Is there a better way to do this?
Components that call useContext directly or indirectly through custom hook will re render every time value of the provider changes.
You can call useContext from a container and let the container render a pure component or have the container call a functional component that is memoized with useMemo.
In the example below count is changed every 100 milliseconds but because useMyContext returns Math.floor(count / 10) the components will only render once every second. The containers will "render" every 100 milliseconds but they will return the same jsx 9 out of 10 times.
const {
useEffect,
useMemo,
useState,
useContext,
memo,
} = React;
const MyContext = React.createContext({ count: 0 });
function useMyContext() {
const { count } = useContext(MyContext);
return Math.floor(count / 10);
}
// simple context that increments a timer
const Context = ({ children }) => {
const [count, setCount] = useState(0);
useEffect(() => {
const i = setInterval(() => {
setCount((c) => c + 1);
}, [100]);
return () => clearInterval(i);
}, []);
const value = useMemo(() => ({ count }), [count]);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
};
//pure component
const MemoComponent = memo(function Component({ count }) {
console.log('in memo', count);
return <div>MemoComponent {count}</div>;
});
//functional component
function FunctionComponent({ count }) {
console.log('in function', count);
return <div>Function Component {count}</div>;
}
// container that will run every time context changes
const ComponentContainer1 = () => {
const count = useMyContext();
return <MemoComponent count={count} />;
};
// second container that will run every time context changes
const ComponentContainer2 = () => {
const count = useMyContext();
//using useMemo to not re render functional component
return useMemo(() => FunctionComponent({ count }), [
count,
]);
};
function App() {
console.log('App rendered only once');
return (
<Context>
<ComponentContainer1 />
<ComponentContainer2 />
</Context>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
With React-redux useSelector a component that calls useSelector will only be re rendered when the function passed to useSelector returns something different than the last time it was run and all functions passed to useSelector will run when redux store changes. So here you don't have to split container and presentation to prevent re render.
Components will also re render if their parent re renders and passes them different props or if parent re renders and the component is a functional component (functional components always re render). But since in the code example the parent (App) never re renders we can define the containers as functional components (no need to wrap in React.memo).
The following is an example of how you can create a connected component similar to react redux connect but instead it'll connect to context:
const {
useMemo,
useState,
useContext,
useRef,
useCallback,
} = React;
const { createSelector } = Reselect;
const MyContext = React.createContext({ count: 0 });
//react-redux connect like function to connect component to context
const connect = (context) => (mapContextToProps) => {
const select = (selector, state, props) => {
if (selector.current !== mapContextToProps) {
return selector.current(state, props);
}
const result = mapContextToProps(state, props);
if (typeof result === 'function') {
selector.current = result;
return select(selector, state, props);
}
return result;
};
return (Component) => (props) => {
const selector = useRef(mapContextToProps);
const contextValue = useContext(context);
const contextProps = select(
selector,
contextValue,
props
);
return useMemo(() => {
const combinedProps = {
...props,
...contextProps,
};
return (
<React.Fragment>
<Component {...combinedProps} />
</React.Fragment>
);
}, [contextProps, props]);
};
};
//connect function that connects to MyContext
const connectMyContext = connect(MyContext);
// simple context that increments a timer
const Context = ({ children }) => {
const [count, setCount] = useState(0);
const increment = useCallback(
() => setCount((c) => c + 1),
[]
);
return (
<MyContext.Provider value={{ count, increment }}>
{children}
</MyContext.Provider>
);
};
//functional component
function FunctionComponent({ count }) {
console.log('in function', count);
return <div>Function Component {count}</div>;
}
//selectors
const selectCount = (state) => state.count;
const selectIncrement = (state) => state.increment;
const selectCountDiveded = createSelector(
selectCount,
(_, divisor) => divisor,
(count, { divisor }) => Math.floor(count / divisor)
);
const createSelectConnectedContext = () =>
createSelector(selectCountDiveded, (count) => ({
count,
}));
//connected component
const ConnectedComponent = connectMyContext(
createSelectConnectedContext
)(FunctionComponent);
//app is also connected but won't re render when count changes
// it only gets increment and that never changes
const App = connectMyContext(
createSelector(selectIncrement, (increment) => ({
increment,
}))
)(function App({ increment }) {
const [divisor, setDivisor] = useState(0.5);
return (
<div>
<button onClick={increment}>increment</button>
<button onClick={() => setDivisor((d) => d * 2)}>
double divisor
</button>
<ConnectedComponent divisor={divisor} />
<ConnectedComponent divisor={divisor * 2} />
<ConnectedComponent divisor={divisor * 4} />
</div>
);
});
ReactDOM.render(
<Context>
<App />
</Context>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [lat, setLat] = useState(0);
const [long, setLong] = useState(0);
const [count, setCount] = useState (0);
const Counter = () => {
setCount(count + 1)
console.log(count)
}
const Location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setLat ( result.coords.latitude)
)
Plugins.Geolocation.getCurrentPosition().then(
result => setLong (result.coords.longitude)
)
}
const interval = () => {
setInterval (() =>
{
Location();
Counter();
}, 5000 );
}
return (
<div>
<div>
<button onClick = {interval}>
Get Location
</button>
</div>
<div>
{long}
</div>
<div>
{lat}
</div>
</div>
)
}
export default Maps;
I'm trying to get the counter to increment on every iteration of setInterval, through the counter function, but when I log count, it does not increment and always remains as 0.
I've tried running setCount itself within setInterval without any success, it still does not increment count.
Its a stale closure. Change to this setCount(prevCount => prevCount + 1).
Using the updater form of set state like above, you can guarantee that you will be using the most recent value of state.
You can think of it as count in your function being a snapshot of what its value was when the setInterval was declared. This will stop your updates from appearing to work.
In addition, setting state is async, so the console.log(count) will most likely not reflect the new value. Log in an effect or outside the function body to see the updated value each render.
A note about your implementation:
You are creating a setInterval each time the button is clicked. This could lead to some interesting side-effects if clicked more than once. If you click the button twice for example, you will have two setIntervals running every 5 seconds.
In addition to #BrianThompson answer. Try this to avoid innecessary rerenders
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [state, setState] = useState({
latLng:{lat:0,lng:0},
counter: 0
})
const interval = useRef()
//Use camelCase for methods
const location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setState ( ({counter}) => {
counter = counter+1
console.log(counter)
return ({
latLng: {
lat: result.coords.latitude,
lng: result.coords.longitude
},
counter
})
})
)
}
const startInterval = () => {
if(interval.current) return;
interval.current = setInterval (() => {
location();
}, 5000 );
}
const stopInterval = () ={
clearInterval(interval.current)
interval.current = null
}
useEffect(()=>{
//Because interval is causing state updates, remember to clear interval when component will unmount
return stopInterval
},[])
return (
<div>
<div>
<button onClick = {startInterval}>
Get Location
</button>
</div>
<div>
{state.latLng.lng}
</div>
<div>
{state.latLng.lat}
</div>
</div>
)
}
I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}