Component keeps re-rendering after state change - javascript

I use handleMouseOver and handleMouseOut functions where I change the value of a state count. However, every time the state is changed the component re-renders instead of just the state. What am I missing here? Thanks.
function foo() {
const [state, setState] = useState({ count: 0, data: {}});
useEffect(() => {
const getData = async () => {
const response = await fetch(url);
const data = await response.json();
setState(prevState => ({
...prevState,
data: data,
}));
};
return ()=>{
getData();
}
}, []);
function handleMouseOver(e) {
setState(prevState => ({
...prevState,
count: e,
}));
};
function handleMouseLeave() {
setState(prevState => ({
...prevState,
count: null,
}));
};
const { count, data } = state;
const BlockComponent = () => {
const data = data.arr;
return (
<Wrapper >
{data.map((value, index) =>
value.map((value, index) => {
return (
<Block
key={index}
val={value}
onMouseEnter={e => handleMouseOver(value)}
onMouseOut={handleMouseLeave}
></Block>
);
})
)}
</Wrapper>
);
};
return (
<Wrapper>
<BlockComponent />
</Wrapper>
);
}
export default foo;

The Issue is with your handleMouseOver function. It is getting executed everytime there is a state Update and the same value is assigned to "count".
All you have to do is place setState inside the condition that will compare the value of event received by the function and the current value of sate.
It should be something like this.
function handleMouseOver(e) {
if (count !== e) {
setState((prevState) => ({
...prevState,
count: e,
}));
}
}

React updates component if component state is changed. That's correct behaviour.
I recommend you to learn react documentation, because component state is a basic concept.

That's one of the main points of state -> component is rerendered when you change state.

Related

How to mock useRef value change after click event in react native with jest and react native testing library

I have a component named MyComponent and it calls 2 APIs when it mounts. 1st API shows the initial data and when I click a particular button, it shows data from 2nd API. I have used useRef to decide which data to show.
const MyComponent = () => {
const random = React.useRef(false);
const { data, isFetching, isError } = useMakeFirstAPICall();
const { data: a, isFetching: b, isError: c, refetch: d } = useMakeSecondAPICall();
const usedData = random.current ? a : data;
const usedIsFetching = random.current ? b : isFetching;
const usedIsError = random.current ? c : isError;
return (
<View>
<Pressable
testID="my.btn"
onPress={() => {
random.current = true;
d();
}}
>
<MyCustomIcon />
</Pressable>
{!usedIsFetching && <ShowData data={usedData} />}
</View>
);
}
I was testing the data changes after the button click. I have managed to mock api calls. But after button click the data is not changing. I tried to mock the useRef value but it's not working. What am I doing wrong here?
This is my test code.
jest.mock("src/api1", () => ({
useMakeFirstAPICall: () => ({
data: MockData1,
}),
}));
jest.mock("src/api2", () => ({
useMakeSecondAPICall: () => ({
data: MockData2,
}),
}));
describe("Testing MyComponent", () => {
test("Test button press action", async () => {
const reference = { current: true };
Object.defineProperty(reference, "current", {
get: jest.fn(() => null),
set: jest.fn(() => null),
});
jest.spyOn(React, "useRef").mockReturnValueOnce(reference);
render(<MyComponent />);
const gridList1 = await screen.findAllByTestId("grid.card");
expect(gridList1).toBeTruthy();
expect(gridList1.length).toBe(5);
const myButton = await screen.findByTestId("my.btn");
await fireEvent(myButton, "click");
const gridList2 = await waitFor(() => screen.findAllByTestId("grid.card"));
expect(gridList2).toBeTruthy();
expect(gridList2.length).toBe(2);
});
});
This is the error it's giving me.

Couldn't correctly initialize state in parent component from children states

I have two React components, namely, Form and SimpleCheckbox.
SimpleCheckbox uses some of the Material UI components but I believe they are irrelevant to my question.
In the Form, useEffect calls api.getCategoryNames() which resolves to an array of categories, e.g, ['Information', 'Investigation', 'Transaction', 'Pain'].
My goal is to access checkboxes' states(checked or not) in the parent component(Form). I have taken the approach suggested in this question.(See the verified answer)
Interestingly, when I log the checks it gives(after api call resolves):
{Pain: false}
What I expect is:
{
Information: false,
Investigation: false,
Transaction: false,
Pain: false,
}
Further More, checks state updates correctly when I click into checkboxes. For example, let's say I have checked Information and Investigation boxes, check becomes the following:
{
Pain: false,
Information: true,
Investigation: true,
}
Here is the components:
const Form = () => {
const [checks, setChecks] = useState({});
const [categories, setCategories] = useState([]);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
useEffect(() => {
api
.getCategoryNames()
.then((_categories) => {
setCategories(_categories);
})
.catch((error) => {
console.log(error);
});
}, []);
return (
{categories.map(category => {
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
/>
}
)
}
const SimpleCheckbox = ({ onCheck, label, id }) => {
const [check, setCheck] = useState(false);
const handleChange = (event) => {
setCheck(event.target.checked);
};
useEffect(() => {
onCheck(check, id);
}, [check]);
return (
<FormControl>
<FormControlLabel
control={
<Checkbox checked={check} onChange={handleChange} color="primary" />
}
label={label}
/>
</FormControl>
);
}
What I was missing was using functional updates in setChecks. Hooks API Reference says that: If the new state is computed using the previous state, you can pass a function to setState.
So after changing:
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
to
const handleCheckChange = (isChecked, category) => {
setChecks(prevChecks => { ...prevChecks, [category]: isChecked });
}
It has started to work as I expected.
It looks like you're controlling state twice, at the form level and at the checkbox component level.
I eliminated one of those states and change handlers. In addition, I set checks to have an initialState so that you don't get an uncontrolled to controlled input warning
import React, { useState, useEffect } from "react";
import { FormControl, FormControlLabel, Checkbox } from "#material-ui/core";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form />
</div>
);
}
const Form = () => {
const [checks, setChecks] = useState({
Information: false,
Investigation: false,
Transaction: false,
Pain: false
});
const [categories, setCategories] = useState([]);
console.log("checks", checks);
console.log("categories", categories);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
};
useEffect(() => {
// api
// .getCategoryNames()
// .then(_categories => {
// setCategories(_categories);
// })
// .catch(error => {
// console.log(error);
// });
setCategories(["Information", "Investigation", "Transaction", "Pain"]);
}, []);
return (
<>
{categories.map(category => (
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
check={checks[category]}
/>
))}
</>
);
};
const SimpleCheckbox = ({ onCheck, label, check }) => {
return (
<FormControl>
<FormControlLabel
control={
<Checkbox
checked={check}
onChange={() => onCheck(!check, label)}
color="primary"
/>
}
label={label}
/>
</FormControl>
);
};
If you expect checks to by dynamically served by an api you can write a fetchHandler that awaits the results of the api and updates both slices of state
const fetchChecks = async () => {
let categoriesFromAPI = ["Information", "Investigation", "Transaction", "Pain"] // api result needs await
setCategories(categoriesFromAPI);
let initialChecks = categoriesFromAPI.reduce((acc, cur) => {
acc[cur] = false
return acc
}, {})
setChecks(initialChecks)
}
useEffect(() => {
fetchChecks()
}, []);
I hardcoded the categoriesFromApi variable, make sure you add await in front of your api call statement.
let categoriesFromApi = await axios.get(url)
Lastly, set your initial slice of state to an empty object
const [checks, setChecks] = useState({});

How to prevent rerender

I made this Component it sends props to checkbox and range components. When I was testing functionality of this 2 components I saw when I made a change in range component checkbox also rerender but it wasn't changed and the same when I change checkbox renage rerender.
Problem
When I change range comp the second one also rerender
const GeneratePassword = () => {
// Checkbox
const [state, setState] = useState({
Symbols: false,
capiatalLetters: false,
digits: false,
})
const handleChange = (e) => {
setState({ ...state, [e.target.name]: e.target.checked })
}
// Range
const [value, setValue] = useState(8)
const handleInputChange = (event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}
const handleBlur = () => {
if (value < 8) {
setValue(8)
} else if (value > 30) {
setValue(30)
}
}
return (
<Comp.StyledCheckboxContainer>
<Comp.CheckboxBorder>
<CheckboxContainer isCheck={handleChange} option={state} />
<Range
value={value}
handleInputChange={handleInputChange}
handleBlur={handleBlur}
setValue={setValue}
/>
</Comp.CheckboxBorder>
</Comp.StyledCheckboxContainer>
)
}
export default GeneratePassword
Components are updated on each state change because their props change. Functions that are used as callbacks should be memoized to prevent this. That some of them rely on current state is a problem, they need to use updater function to avoid referring to stale state:
const handleChange = useCallback((e) => {
setState((state) => ({ ...state, [e.target.name]: e.target.checked }))
}, [])
...
const handleInputChange = useCallback((event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}, [])
...
const handleBlur = useCallback(() => {
setValue(value => {
if (value < 8) {
return 8
} else if (value > 30) {
return 30
}
})
}, [])
At this point child components are able to prevent unnecessary rerenders. If they don’t do this, they need to be additionally wrapped with React.PureComponent or React.memo. For arbitrary component a wrapper can be:
OptimizedComp = React.memo(props => <Comp {...props} />);
It is because your callback for Checkbox component is defined inside parent which re-renders whenever range component changes causing a change is callback for Checkbox component since it gets new value on every render.
const handleInputChange = (event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}
On every render, handleInputChange gets a new value, a new reference. You need to use React.useCallbak() to keep the same value for all renders
const handleInputChange = React.useCallback((event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}, []);

Child component causing too many renderings on parent component when changing global state in React

I am working on a React web application, I have a global state for storing some data, for some reason I happen to see quite unnecessarily renderings on my app!
I have component that looks like this:
const LanguageSkills = () => {
const [languageSelected, setLanguage] = React.useState({})
const { dispatch } = useContext(GlobalStore)
const handleChange = (name: string) => (event: any) => {
setLanguage({ ...languageSelected, [name]: event.target.checked })
}
useEffect(() => {
dispatch({
type: ApplicantActions.FILTER_APPLICANTS,
payload: {
languageSkills: languageSelected,
},
})
}, [dispatch, languageSelected])
return (
<>
{
{languageSkills.map((lang, index) => (
<TermWrapper key={index}>
<GreenCheckbox onChange={handleChange(lang.term)} value="checked" />
<Term>{lang.term}</Term>
</TermWrapper>
))}
}
</>
)
}
This component causes the parent component to render without any action doing from this component, well because the dispatch function is called immediately inside useEffect() once the component gets mounted (at least what I know), how else can I handle this so dispatch only gets called when I click the radio button? Is there any callback or someway that I can call dispatch once the local state changed using setState()?
Any idea how can I use dispatch in such situations?
Update this line
<GreenCheckbox onChange={handleChange(lang.term)} value="checked" />
to this
<GreenCheckbox onChange={() => handleChange(lang.term)} value="checked" />
you are calling the function in render time. give it a callback
UPDATED:
const handleChange = (name: string) => (event: any) => {
setLanguage({ ...languageSelected, [name]: event.target.checked })
}
useEffect(() => {
callMe();
}, [languageSelected])
const callMe = () => {
dispatch({
type: ApplicantActions.FILTER_APPLICANTS,
payload: {
languageSkills: languageSelected,
},
})
}
After several trying I came up with this and it seems fine for me, does not cause anymore parent component to re-render!
const LanguageSkills = () => {
const [isVisible, setIsVisible] = useState(false)
const [state, setState] = React.useState({})
const { dispatch } = useContext(GlobalStore)
const handleChange = (name: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const obj = { ...state, [name]: event.target.checked }
dispatchLanguageFilter(obj)
setState({ ...state, [name]: event.target.checked })
}
const dispatchLanguageFilter = useCallback(
obj => {
const languageSkills: string[] = []
for (const [key, value] of Object.entries(obj)) {
if (value) {
languageSkills.push(key)
}
}
dispatch({
type: ApplicantActions.FILTER_APPLICANTS,
payload: {
languageSkills: languageSkills,
},
})
},
[dispatch],
)
return (
<>
<Wrapper onClick={() => setIsVisible(!isVisible)}>
<FilterContainerTitle>Kielitaito</FilterContainerTitle>
<Arrow>{!isVisible ? <ArrowDown /> : <ArrowUp />}</Arrow>
</Wrapper>
{isVisible && (
<>
{languageSkills.map((lang, index) => (
<TermWrapper key={index}>
<GreenCheckbox onChange={handleChange(lang.term)} value="checked" />
<Term>{lang.term}</Term>
<TotalAmountWrapper>
<TotalAmount>{lang.totalAmount}</TotalAmount>
</TotalAmountWrapper>
</TermWrapper>
))}
</>
)}
</>
)
}
Not sure if its right way but mission accomplished!

React hooks useState to set multiple states in context provider

I have a createContext component that is using useState to set multiple values returned from a fetch function. However in my code below, when a state is updated, the others states return to the original value.
For example, in getCountryCode() the state is updated for countryCode, but then iconCode in weatherInit() fetches its value and countryCode returns to the original US.
import React, { createContext, useState, useEffect } from 'react';
export const GlobalConsumer = createContext();
export const GlobalProvider = ({ children }) => {
const [state, setState] = useState({
menuPanel: false,
countryCode: 'US',
weatherLoading: true,
iconCode: '',
fahrenheit: '',
celcius: '',
showCelcius: false
});
const getCountryCode = () => {
const url = `https://ipapi.co/json/`;
fetch(url)
.then(data => data.json())
.then(data => {
const countryCode = data.country;
setState({ ...state, countryCode });
});
};
const weatherInit = () => {
const CITY_LAT = '...';
const CITY_LON = '...';
const OW_KEY = '...';
const url = `//api.openweathermap.org/data/2.5/weather?lat=${CITY_LAT}&lon=${CITY_LON}&units=imperial&appid=${OW_KEY}`;
fetch(url)
.then(data => data.json())
.then(data => {
const iconCode = data.weather[0].id;
setState({ ...state, iconCode });
const fahrenheit = Math.round(data.main.temp_max);
setState({ ...state, fahrenheit });
const celcius = Math.round((5.0 / 9.0) * (fahrenheit - 32.0));
setState({ ...state, celcius });
setTimeout(() => {
setState({ ...state, weatherLoading: false });
}, 150);
});
};
useEffect(() => {
getCountryCode();
weatherInit();
}, []);
return (
<GlobalConsumer.Provider
value={{
contextData: state,
togglemMenuPanel: () => {
setState({ ...state, menuPanel: !state.menuPanel });
},
toggleCelcius: () => {
setState({ ...state, showCelcius: !state.showCelcius });
}
}}
>
{children}
</GlobalConsumer.Provider>
);
};
I believe that this is caused because each value requires it's own useState. However, can these values be merged or is there another way to achieve this outcome, where I am only required to pass as data to the Provider context?
It's because you are using the old value of state when calling setState(). As documented here (Scroll down to the "Note"-block) you have to pass a function to your setState call:
const iconCode = data.weather[0].id;
setState(prevState => ({ ...prevState, iconCode }));
const fahrenheit = Math.round(data.main.temp_max);
setState(prevState => ({ ...prevState, fahrenheit }));
const celcius = Math.round((5.0 / 9.0) * (fahrenheit - 32.0));
setState(prevState => ({ ...prevState, celcius }));
setTimeout(() => {
setState(prevState => ({ ...prevState, weatherLoading: false }));
}, 150);
Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});

Categories