Why doesn't the local storage change happen on first click?
const Menu = () => {
const { auth, setAuth } = useContext(AuthContext);
useEffect(() => {
localStorage.setItem("theme", auth.theme);
}, [auth.theme]);
return (
<header>
<ToggleSwitch
defaultChecked={auth.theme}
onChange={() => {
setAuth((prevState) => ({
...prevState,
theme: !prevState.theme,
}));
}}
/>
</header>
);
};
I have seen the same question asked for setState and the answer seems to be useEffect and ...prevState which I've done.
Edit: It in fact updates from true to false first time but not the other way around.
Related
I have (what seems to be) a very peculiar situation where I seem to be getting extra events emitted based on my Redux state.
I have narrowed the behavior down to whether or not I make a successful request to my /users endpoint and retrieve a list of users which is then stored in Redux.
If the commented code is not active (as it is currently shown), I am able to successfully render the modal(s) reliably and step between states.
If the commented code is active, the (which is what is behind the as well) emits an onDismiss call immediately. This has the result of closing the modal immediately.
If the commented code is active, but the response from the thunk is a 401 and the user data is not loaded (i.e., the state of the user key in redux is a failure, not success, then the modal works -- though of course, there are no users to select.
I have confirmed this behavior is consistent no matter where I seem to make this fetch request (initially it was in the App.tsx to be called immediately. I also tried it in an intermediate component).
Question(s):
Can you explain why I might be getting different behavior in my click handlers based on what is in my state?
Is there something I'm missing and I'm conflating my Redux state with the actual behavior?
I know I can solve this by adding a event.stopPropagation() call in strategic places (e.g., on the first button that opens the <ConfirmationBox> and then again on the button in the <ConfirmationBox> that transitions to the SelectUser modal), but are there other solutions?
//pinFlow.tsx
type States =
| { state: 'Confirm' }
| { state: 'SelectUser' }
| { state: 'SubmitPin'; user: User };
export function pinFlow<T extends ConfirmationBoxProps>(
ConfirmationBox: React.FC<T>,
authorization: Authorization,
) {
const [state, setState] = React.useState<States>({ state: 'Confirm' });
// const dispatch=useDispatch();
// initialize users
// const users = useSelector((state: InitialState) => state.pinAuth.users);
// const fetchUsers = useCallback(() => {
// dispatch(fetchUsersThunk());
// }, [dispatch]);
// useEffect(() => {
// if (users.state === RemoteDataState.NotStarted) {
// fetchUsers();
// }
// }, [fetchUsers, users.state]);
return (props: T) => {
const users = useSelector((state: InitialState) =>
mapRemoteData(state.pinAuth.users, users =>
users.filter(user => user.authorizations.includes(authorization)),
),
);
switch (state.state) {
case 'Confirm': {
return (
<ConfirmationBox
{...props}
onSubmit={(_event: React.MouseEvent) => {
setState({ state: 'SelectUser' });
}}
/>
);
}
case 'SelectUser': {
return (
<Modal
title={'PIN Required'}
canClickOutsideToDismiss={true}
onDismiss={() => {
setState({ state: 'Confirm' });
}}
>
<p className={style.selectProfileText}>Select your profile:</p>
<pre>
<code>{JSON.stringify(users, null, 4)}</code>
</pre>
{/*
<UserList users={users.data} /> */}
</Modal>
);
}
default: {
return <Modal title="others">all others</Modal>;
}
}
};
}
The code is used in another component like so:
function Comp(){
const [selected, setSelected] = useState();
const [mode, setMode] = useState();
const ConfirmationModal =
protected
? pinFlow(MenuItemModal, permission)
: MenuItemModal;
return(
<ConfirmationModal
item={selected}
mode={mode}
disabled={availability.state === RemoteDataState.Loading}
errorMessage={tryGetError(availability)}
onCancel={() => {
setMode(undefined);
dispatch(resetAvailability());
}}
onSubmit={(accessToken: string) => {
dispatch(findAction(selected, mode, accessToken));
}}
/>
)
}
I am using the React useState hook to update a list of items. I would like for only the added/updated components to be rendered but everytime the state of of the list changes all the items in list are re-rendered.
I have followed Preventing list re-renders. Hooks version. to solve the re-render issue but it doesn't work
Can someone help me understand, what's wrong with the below code or if this is actually not the right way to do it
function App() {
const [arr, setArr] = useState([])
useEffect(() => {
//getList here returns a list of elements of the form {id: number, name: string}
setArr(getList());
}, [])
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, [arr])
return (
<div className="App">
{
arr.map((item) => {
return (
<NewComp key={`${item.id}`} item={item} clickHandle={clickHandle} />
);
})
}
</div>
);
}
const NewComp = ({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
}
The reason all your NewComp re-render is because your clickHandle function is being recreated whenever there is any change in the state arr.
This happens because you have added arr as a dependency to useCallback. This however is not required.
Once you fix it, you can wrap your NewComp with React.memo to optimize their re-renders. Also you must note that call the render function of a component is different from actually re-rendering it in the DOM.
const clickHandle = useCallback((e, id) => {
e.preventDefault()
setArr((arr) => {
return [...arr, {
id: id + 100,
name: `test${id+100}`
}]
})
}, []);
const NewComp = React.memo({
item,
clickHandle
}) => {
return (
<div>
<button onClick={(e) => clickHandle(e, item.id)}>{item.name}</button>
</div>
);
});
So I have a problem, I've been stuck on for a couple of hours. My state doesn't get updated inside a function. As you can see in my example I have a useState hook which is responsible for keeping the value of the text input. Let's say I type in 'abcd', if i console log the state in the handleChange and outside of it just before return, the state shows correctly, however on the handleHeaderRightButtonPress which is responsible for the saving functionality basically, it doesn't update, it's always my default value, in this case randomVal. Any ideeas why this behaviour could happen or how could i troubleshoot it? Thanks in advance:
My example(I stripped out unnecessary code so it's easier)
const TextAreaScreen = ({ navigation, route }) => {
const placeholder = route.params?.placeholder;
const [value, setValue] = useState('randomval');
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerRight: () =>
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
});
}, []);
const handleChange = (value: string) => {
console.log('here', value); //the updated value shows up correctly
setValue(value);
};
const handleHeaderRightButtonPress = () => {
const onFinish = route.params?.onFinish;
console.log('value in handleFunc', value); // the updated values does NOT work here
onFinish(value);
navigation.goBack();
};
console.log('state val::', value); // updated value shows up correctly
return (
<TextArea
value={value}
placeholder={placeholder}
onChangeText={handleChange}
/>
);
};
export default TextAreaScreen;
Pass value to useEffect like :
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerRight: () =>
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
});
}, [value]);
Just noticed I wasn't updating my useEffect with the value. Fixed by adding it as a dependency in the array:
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerLeft: () => {
const onBackPress = () => {
navigation.goBack();
};
return Platform.select({
ios: (
<NavigationHeader.TextButton
label={t('general.cancel')}
onPress={onBackPress}
/>
),
android: (
<NavigationHeader.IconButton
iconName="times"
label={t('general.cancel')}
onPress={onBackPress}
/>
)
});
},
headerRight: () =>
Platform.select({
ios: (
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
),
android: (
<NavigationHeader.IconButton
iconName="check"
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
)
})
});
}, [value]); // here
https://codesandbox.io/s/react-usecontextuseeffect-stale-data-bug-l81sn
In a component I use useEffect to do something when a certain value changes. Now it's only the value of a simple counter, but in the real world it would be a array where items are removed or added etc. A little bit more complex.
In the useEffect I also have a resize detection. Again, in this example not very interesting.
const App = props => {
const [count, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}, 0);
return (
<CountContext.Provider value={{ count, dispatch }}>
<div className="App">
<h1>App</h1>
<Counter />
</div>
</CountContext.Provider>
);
};
const Counter = () => {
const counter = useContext(CountContext);
useEffect(() => {
window.addEventListener('resize', () => {
console.log(counter.count);
})
},[counter])
return (
<div className="Counter">
<p>Counter: {counter.count}</p>
<input
type="button"
value="+"
onClick={() => counter.dispatch({ type: 'INCREMENT' })}
/>
<input
type="button"
value="-"
onClick={() => counter.dispatch({ type: 'DECREMENT' })}
/>
</div>
);
};
The issue is that when I resize the viewport the console.log(counter.count) shows all previous values:
The issue is a memory leak in the useEffect() method. You need to clean up on re-renders. I forked your sandbox and tested it with this and it works as expected:
useEffect(() => {
const resizeEvent = () => {
console.log(counter.count);
};
window.addEventListener("resize", resizeEvent);
return () => {
window.removeEventListener("resize", resizeEvent);
};
}, [counter]);
Notice the return for the clean up and the refactor of your code to a called function so that it can be correctly removed on dismount.
You add a new one eventListener every time your state changes. Three changes - three eventListeners. Moreover, when your Counter component is unmounted, the listeners keep alive that cause memory leaks.
First of all, you can take this part outside of useEffect:
window.addEventListener('resize', () => {
console.log(counter.count);
})
Or you should use empty array as dependencies list in useEffect, then it will fire just once:
useEffect(() => {
}, []) // empty array here says 'do it once'
And finally, useEffect is a perfect place for fetching data or subscribing for events etc. But do not forget to clear all it up after component is not needed anymore. For doing this, return cleaning function in useEffect:
useEffect(() => {
// your main logic here
...
// cleaning up function:
return () => {
removeEventListener, unsubscribe etc...
}
}, [])
I have a weird bug that only happens some of the time - onChange fires but does not change the value. Then if I click outside of the input with the onChange function, then click back inside the input box, the onChange function starts working.
The onChange function is like so:
const handleBarAmountChange = (event) => {
let newWidthAmount = event.target.value / 10;
setNewWidth(newWidthAmount);
setNewBarAmount(event.target.value);
};
A parent div is using a ref with useRef that is passed to this function:
import { useEffect, useState } from 'react';
const useMousePosition = (barRef, barInputRef, barContainerRef) => {
const [ mouseIsDown, setMouseIsDown ] = useState(null);
useEffect(() => {
const setMouseDownEvent = (e) => {
if (e.which == 1) {
if (barContainerRef.current.contains(e.target) && !barInputRef.current.contains(e.target)) {
setMouseIsDown(e.clientX);
} else if (!barInputRef.current.contains(e.target)) {
setMouseIsDown(null);
}
}
};
window.addEventListener('mousemove', setMouseDownEvent);
return () => {
window.removeEventListener('mousemove', setMouseDownEvent);
};
}, []);
return { mouseIsDown };
};
Is the onChange conflicting somehow with the eventListener?
How do I get round this?
There were a few syntax errors and missing hook dependencies that were the cause of your bugs. However, you can simplify your code quite a bit with a few tweaks.
When using state that relies upon other state, I recommend lumping it into an object and using a callback function to synchronously update it: setState(prevState => ({ ...prevState, example: "newValue" }). This is similar to how this.setState(); works in a class based component. By using a single object and spreading out it properties ({ ...prevState }), we can then overwrite one of its properties by redefining one of them ({ ...prevState, newWidth: 0 }). This way ensures that the values are in sync with each other.
The example below follows the single object pattern mentioned above, where newWidth, newBarAmount and an isDragging are properties of a single object (state). Then, the example uses setState to update/override the values synchronously. In addition, the refs have been removed and allow the bar to be dragged past the window (if you don't want this, then you'll want to confine it within the barContainerRef as you've done previously). The example also checks for a state.isDragging boolean when the user left mouse clicks and holds on the bar. Once the left click is released, the dragging is disabled.
Here's a working example:
components/Bar/index.js
import React, { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import "./Bar.css";
function Bar({ barName, barAmount, colour, maxWidth }) {
const [state, setState] = useState({
newWidth: barAmount / 2,
newBarAmount: barAmount,
isDragging: false
});
// manual input changes
const handleBarAmountChange = useCallback(
({ target: { value } }) => {
setState(prevState => ({
...prevState,
newWidth: value / 2,
newBarAmount: value
}));
},
[]
);
// mouse move
const handleMouseMove = useCallback(
({ clientX }) => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
newWidth: clientX > 0 ? clientX / 2 : 0,
newBarAmount: clientX > 0 ? clientX : 0
}));
}
},
[state.isDragging]
);
// mouse left click hold
const handleMouseDown = useCallback(
() => setState(prevState => ({ ...prevState, isDragging: true })),
[]
);
// mouse left click release
const handleMouseUp = useCallback(() => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
isDragging: false
}));
}
}, [state.isDragging]);
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [handleMouseMove, handleMouseUp]);
return (
<div className="barContainer">
<div className="barName">{barName}</div>
<div
style={{ cursor: state.isDragging ? "grabbing" : "pointer" }}
onMouseDown={handleMouseDown}
className="bar"
>
<svg
width={state.newWidth > maxWidth ? maxWidth : state.newWidth}
height="40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
colour={colour}
>
<rect width={state.newWidth} height="40" fill={colour} />
</svg>
</div>
<div className="barAmountUnit">£</div>
<input
className="barAmount"
type="number"
value={state.newBarAmount}
onChange={handleBarAmountChange}
/>
</div>
);
}
// default props (will be overridden if defined)
Bar.defaultProps = {
barAmount: 300,
maxWidth: 600
};
// check that passed in props match patterns below
Bar.propTypes = {
barName: PropTypes.string,
barAmount: PropTypes.number,
colour: PropTypes.string,
maxWidth: PropTypes.number
};
export default Bar;
React uses SyntheticEvent and Event Pooling, from the doc:
Event Pooling
The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.
You could call event.persist() on the event or store the value in a new variable and use it as follows:
const handleBarAmountChange = (event) => {
// event.persist();
// Or
const { value } = event.target;
let newWidthAmount = value / 10;
setNewWidth(newWidthAmount);
setNewBarAmount(value);
};