When I dispatch, payload(value) goes back to old value in React - reactjs

// EditableNote.js
function EditableNote({ note }) {
const [editableNote, setEditableNote] = useState(note);
const { title, content } = editableNote;
const dispatch = useDispatch();
useEffect(() => {
setEditableNote(note);
}, [note]);
›
useEffect(() => {
dispatch(saveEditableNote(editableNote)); // I think here is problem
}, [dispatch, editableNote]);
const handleBlur = e => {
const name = e.target.id;
const value = e.currentTarget.textContent;
setEditableNote({ ...editableNote, [name]: value });
};
return (
<EditNote spellCheck="true">
<NoteTitle
id="title"
placeholder="Title"
onBlur={handleBlur}
contentEditable
suppressContentEditableWarning={true}>
{title}
</NoteTitle>
<NoteContent
id="content"
placeholder="Note"
onBlur={handleBlur}
contentEditable
suppressContentEditableWarning={true}>
{content}
</NoteContent>
</EditNote>
);
}
export default EditableNote;
I have EditableNote component which is contentEditable. I set its initial state through props from its parent(Note). So if something is changed in note, then editableNote has to changed.
To keep recent props state, I use useEffect. Everything seems working well.
Here is an issue. If I first change color of note and typing, it is updated as expected. But on contrast, if I first typing and change color, editableNote state is not updated.
// Reducer.js
case actions.GET_NOTE_COLOR:
return {
...state,
bgColor: action.payload
}
case actions.CHANGE_NOTE_COLOR:
return {
...state,
notes: state.notes.map(note => note.id === action.payload ?
{ ...note, bgColor: state.bgColor }
: note
)
};
case actions.SAVE_EDITABLE_NOTE: // payload is old value
return {
...state,
editableNote: action.payload,
}
I check what happened in an action. I found everything works until CHANGE_NOTE_COLOR but when dispatch SAVE_EDITABLE_NOTE, its payload is not updated!
I have no idea.. plz.. help me...TT

You have to use the connect wrapper provided by redux to connect actions and state of redux to your components
https://react-redux.js.org/api/connect

Related

Best approach in updating a useReducer state in reaction to a change in its initialArg

After the first render, the useReducer hook doesn't react to changes in its initialArg (second positional) argument. It makes it hard to properly sync it with an external value, without having to rely on an extra cycle by dispatching a reset action inside a useEffect hook.
I built a minimal example. It's a simple, formik-like, form provider. Here's what it looks like:
// App.js
const users = {
1: {
firstName: 'Paul',
lastName: 'Atreides',
},
2: {
firstName: 'Duncan',
lastName: 'Idaho',
},
};
const App = () => {
const [id, setId] = useState(1);
return (
<>
<div>Pick User</div>
<button onClick={() => { setId(1); }} type="button">User 1</button>
<button onClick={() => { setId(2); }} type="button">User 2</button>
<FormProvider initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
const [values, dispatch] = useReducer(reducer, initialValues);
const handleChange = useCallback((evt) => {
dispatch({
field: evt.target.name,
type: 'UPDATE_FIELD',
value: evt.target.value,
});
}, []);
return (
<FormContext.Provider value={{ handleChange, values }}>
{children}
</FormContext.Provider>
);
};
// Editor.js
const Editor = () => {
const { handleChange, values } = useContext(FormContext);
return (
<>
<div>First name:</div>
<input
name="firstName"
onChange={handleChange}
value={values.firstName}
/>
<div>First name:</div>
<input
name="lastName"
onChange={handleChange}
value={values.lastName}
/>
</>
);
};
If you open the demo and click on the User 2 button, you'll notice that nothing happens. It's not surprising since we know that the useReducer hook gets initialised once using the provided initialArg argument and never reads its value again.
What I expect is the useReducer state to reflect the new initialArg prop, i.e. I want to see "Duncan" in the First name input after clicking on the User 2 button.
From my point of vue, I can see two options:
1. Passing a key prop to the FormProvider component.
// App.js
const App = () => {
// ...
return (
<>
{/* ... */}
<FormProvider key={id} initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
This will indeed fix the problem by destroying and re-creating the FormProvider component (and its children) every time the id changes. But it feels like a hack to me. Plus, it seems inefficient to rebuild that entire part of the tree (which is substantial in the real application) just to get that input values updated. However, this seems to be a common fix for such problems.
2. Dispatch a RESET action whenever initialValues changes
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return action.values;
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
// ...
const isFirstRenderRef = useRef(true);
useEffect(() => {
if (!isFirstRenderRef.current) {
dispatch({
type: 'RESET',
values: initialValues,
});
}
}, [initialValues]);
useEffect(() => {
isFirstRenderRef.current = false;
}, []);
// ...
};
This will work as well, but, because it's happening inside a useEffect hook, it will require an extra cycle. It means that there'll be a moment where the form will contain stale values. If the user types at that moment, it could cause a race condition.
3. Idea
I read in this article by Mark Erikson that:
Function components may call setSomeState() directly while rendering, as long as it's done conditionally and isn't going to execute every time this component renders. [...] If a function component queues a state update while rendering, React will immediately apply the state update and synchronously re-render that one component before moving onwards.
So it seems that I should be able to call dispatch({ type: RESET, values: initialValues }); directly from the body of the function, under the condition that initialValues did change (I'd use a ref to keep track of its previous value). This should result in the state being updated in just one cycle. However, I couldn't get this to work.
——
What do you think is best between option 1, 2 and (3). Any advice/guidance on how I should address this problem?

React useReducer not updating object in array

Todos Component:
import React, { useReducer, useState } from 'react';
import Todo2 from './Todo2';
export const ACTIONS = {
ADD_TODO : 'add-todo',
TOGGLE_TODO : 'toggle-todo'
};
function reducer(state, action) {
switch(action.type) {
case ACTIONS.ADD_TODO:
return [...state, {id: Date.now(), name: action.payload.name, complete: false}];
case ACTIONS.TOGGLE_TODO:
const patch = [...state];
console.log('The index is:', action.payload.index);
console.log('The current state is:', patch[action.payload.index].complete);
// update state
patch[action.payload.index].complete = !patch[action.payload.index].complete;
console.log('The updated state is:', patch[action.payload.index].complete);
console.log('The patch is:', patch);
return patch;
}
}
export default function Todos() {
const [todos, dispatch] = useReducer(reducer, []);
const [name, setName] = useState('');
function handleSubmit(e) {
e.preventDefault();
dispatch({type: ACTIONS.ADD_TODO, payload: {name: name}});
setName('');
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={name} onChange={e => setName(e.target.value)}/>
</form>
{todos.map((todo, i) => <Todo2 key={todo.id} todo={todo} index={i} dispatch={dispatch} />)}
</div>
);
}
Todo Component:
import React from 'react';
import { ACTIONS } from './Todos2';
export default function Todo2({ todo, dispatch, index }) {
return (
<div>
<span style={{color: todo.complete ? 'green':'red' }}>{todo.name}</span>
<button onClick={e => dispatch({type: ACTIONS.TOGGLE_TODO, payload : {index: index}})}>Toggle</button>
</div>
)
}
I am trying to update an object inside an array, setting its "complete" property to either true or false depending on its current value. I console.logged the results but in the end the patch never gets it's updated data, it always retains it's original value.
If I update the state like this, it works and I don't know why this works but the index way of updating does not.
// update state
patch[action.payload.index].complete = true;
I created a codesandbox example and reproduced the same issue, then I changed const patch = [...state] to:
import _ from 'lodash'
...
...
const patch = _.cloneDeep(state)
And, everything else staying the same, it worked like a charm. Here is the code Now I know that, spread operator ..., does create a shallow copy rather than a deep copy. Therefore, I think your !patch[action.payload.index].complete is updated in the same line, creating a paradoxical assignment (like a double update). Couldn't find a technical reference to explain this better, but the issue is for sure not deep copying the object.
Suggestion
case ACTIONS.TOGGLE_TODO:
return state.map((el, idx) => idx === action.payload.index ? {...el, complete: !el.complete} : el)

React Native modal is not updated when Redux state is changed using hooks

I have a modal component in my React Native mobile app. It receives an array of objects from Redux state. I can delete a specific item in the array using dispatching an action using useDispatch hook. However, after sending the delete action, the component state is not updated automatically, so that I have to reopen the modal every time to see the updated list.
How can I set the modal to automatically re-render when the redux state is changed using dispatch?
SelectedItems.js
const SelectedItems = () => {
const vegetables = useSelector(state => state.new_order.vegetables)
return (
<Modal visible={isVisible}>
{vegetables.map( (v,index) =>
<VegeItem
key={index}
index={index}
name={v.name}
qty={v.qty}
metric={v.metric}
removeItem={(index) => {
dispatch({
type: 'DELETE_VEGE',
id: index
})
}}
/>)}
</View>
</Modal>
)
}
newOrderReducer.js
const newOrderReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'ADD_VEGE':
let updatedList = [...state.vegetables,action.vege]
return {
...state,
vegetables: updatedList
}
case 'DELETE_VEGE':
let newVegeList = state.vegetables
newVegeList.splice(action.id,1)
return {
...state,
vegetables: newVegeList
}
default:
return state
}
};
while doing like so let newVegeList = state.vegetables, newVegeList is just a pointer on your state and not a shallow copy of it. Therefore, you still can't mutate it as you can't mutate state outside the return part of the reducer.
so you can do like let newVegeList = [...state.vegetables], or directly at the return
return {
...state,
vegetables: state.vegetables.filter((veg, i) => i != action.id)
}
you can also send veg name or whatever and modify the checker at filter

Custom hook reset to its initial state

Hook
export const useCreateAccount = () => {
const [state, setState] = useState(initialState)
const onChangeInput: ChangeEventFunction = useCallback(({ target }) => {
if (!target.files) {
return setState({ ...state, [target.name]: target.value })
}
setState({ ...state, [target.name]: target.files[0] })
}, [])
return { onChangeInput }
}
Component
const { onChangeInput } = useCreateAccount()
<form>
<input name="name1" onChange={onChangeInput}>
<input name="name2" onChange={onChangeInput}>
</form>
Every time I do some change in second input(name2) the previous state(name1) of the component has been lost(reset to initial state), The reason I use 'useCallback', I only need one instance of 'onChangeInput'
But if I remove 'useCallback', state is keeping the previous values(name1)
I can't understand this behavior in hooks, can someone elaborate more on this?
From the docs:
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
Here, when you are using useCallback, the function has been defined in it's initial render and has the initial state defined then. This is the reason why useCallback has a depedency array that can be used to refresh the function and values used inside it.
But you cannot use state as a dependency because you are setting the same inside it, instead you can use the functional version of setState so as to get the previous values of state instead of reffering to the central one.
const onChangeInput: ChangeEventFunction = useCallback(({ target }) => {
if (!target.files) {
return setState(prevState => ({ ...prevState, [target.name]: target.value }));
}
setState(prevState => ({ ...prevState, [target.name]: target.files[0] }))
}, [])

How to clear inputs after useReducer

I am using useReducer to control my 2 inputs:
const [noteInput, setNoteInput] = useReducer(
(state, newState) => ({ ...state, ...newState }),
{
title: '',
content: ''
}
);
After onClick on button i want both inputs to be cleared. How can i do that after useReducer?
You can update your state to empty using setState OR You can dispatch other actions for updating that state to the empty string.
https://redux.js.org/basics/reducers#reducers
If you are just looking to use useReducer to clear your form you can use dispatch to do so. Let's use this example button component as an example.
//Component
<Button onClick={() => {dispatch({ type: "CLEAR_FORM"})}}>Submit</Button>
After clicking the button "CLEAR_FORM" is dispatched to the reducer.
//Form Initial State & Reducer switch statement
export const initialState={
username:"",
password:""
}
export const reducer = (state, action) => {
switch(action.type){
case: "CLEAR_FORM":
return {
username:"",
password:"",
}
default:
return state
}
}
When the reducer gets the { type: "LOG_OUT" }, in this case, it resets the username and password fields to an empty string.
https://reactjs.org/docs/hooks-reference.html#usereducer
You can pass a third argument to React.useReducer() called init. This is called lazy initialization and is a quick way to revert to the initial state.
https://reactjs.org/docs/hooks-reference.html#lazy-initialization
If you have a Button, you only need to alter onSubmit={handleSubmit} and have the function below and things will be all good. Just remember not to set defaultValue of any TextField or what you are using, just set the value only
const handleReset = (event) => {
event.preventDefault();
Object.keys(formInput).forEach((inputKey) => {
setFormInput({ [inputKey]: '' });
});
};

Resources