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]: '' });
});
};
Related
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?
I'm currently working on a poll app which is supposed to sequentially render a list of questions and post answers to the server. I have no problem handling answers but looping through questions gave me some trouble.
Here is a flow of my code:
PollContainer.js - component
import React, { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Question from './Questions/Question';
import { pushAnswers } from '../../actions/answers';
import { incrementCounter } from '../../actions/utils';
import Thanks from './Thanks'
const PollContainer = () => {
const dispatch = useDispatch();
const questions = useSelector(state => state.questions); // an array of questions
// a counter redux state which should increment at every click of 'submit' inside a question
const utils = useSelector(state => state.utils);
let activeQuestion = questions[utils.counter];
// function passed as a prop to a singular Question component to handle submit of an answer
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers;
answersUpdate.push(answer);
console.log(`counter:${utils.counter}`) // logs 0 everytime I click submit, while redux dev tools show that utils.counter increments at every click
if (utils.counter < questions.length ) {
setState({...state, answers: answersUpdate, activeQuestion: activeQuestion});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({...state, isFinished: true});
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer
})
return (
(utils.isLoading) ? (
<h1>Loading questions...</h1>
) : (
<div>
{(!state.isFinished && <Question { ...state }/>)}
{(state.isFinished && <Thanks/>)}
</div>
))
}
export default PollContainer;
incrementCounter action:
import * as types from "./types";
export const incrementCounter = () => {
return {
type: types.INCREMENT_COUNTER,
}
}
utils.js - reducer
// reducer handles what to do on the news report (action)
import * as types from '../actions/types';
const initialState = {
isLoading: false,
error: null,
counter: 0
}
export default (utils = initialState, action) => {
switch(action.type){
case types.LOADING_DATA:
return {...utils, isLoading: true};
case types.DATA_LOADED:
return {...utils, isLoading: false};
case types.ACTION_FAILED:
return {...utils, error: action.error};
case types.INCREMENT_COUNTER:
return {...utils, counter: utils.counter + 1} // here is the incrementing part
default:
return utils;
}
}
utils.counter that is passed to pushSingleAnswer function doesn't increment, however redux dev tools tells me it does increase every time I click submit in a Question component. Because of that it doesn't render next questions. The submit handler in Question component is simply this:
const handleSubmit = (e) => {
e.preventDefault();
props.pushSingleAnswer(state);
};
I also tried with:
useEffect(() => {
dispatch(incrementCounter())},
[state.answers]
);
expecting it'll increment every time there's an update to state.answers but it doesn't work either. Morover the counter in redux-dev-tools doesn't increment either.
I'd be very grateful for any suggestions, this is my first serious react-redux project and I really enjoy working with these technologies. However I do not quite understand how react decides to render stuff on change of state.
Issue
You are closing over the initial counter state in the pushSingleAnswer callback stored in state and passed to Question component.
You are mutating your state object in the handler.
Code:
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers; // <-- save state reference
answersUpdate.push(answer); // <-- state mutation
console.log(`counter:${utils.counter}`) // <-- initial value closed in scope
if (utils.counter < questions.length ) {
setState({
...state, // <-- persists closed over callback/counter value
answers: answersUpdate,
activeQuestion: activeQuestion,
});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer // <-- closed over in initial state
});
{(!state.isFinished && <Question { ...state }/>)} // <-- stale state passed
Solution
Don't store the callback in state and use functional state updates.
const pushSingleAnswer = (answer) => {
console.log(`counter:${utils.counter}`) // <-- value from current render cycle
if (utils.counter < questions.length ) {
setState(prevState => ({
...prevState, // <-- copy previous state
answers: [
...prevState.answers, // <-- copy previous answers array
answer // <-- add new answer
],
activeQuestion,
}));
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
});
{!state.isFinished && (
<Question { ...state } pushSingleAnswer={pushSingleAnswer} />
)}
// 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
I created a custom hook to store Objects in a useState hook and allow changing properties without loosing the other entries.
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
setState({
...state,
...newState
});
}
];
};
This hook works in my component but doesn't when I assign it to my context.
Here is what I did:
I created a context:
export const navigation = createContext();
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/store.js:40-83
I created a useObject variable and assigned it as value to my Context Provider
<navigation.Provider value={useObject()}>
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/Layout.js:234-284
I load the context via useContext and change its value
const [navigationState, setNavigationState] = useContext(navigation);
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js:476-616
Result:
The context always stores the new entry and removes all existing entries.
Anyone knows why ?
Here is the Sandbox link. You can test it by clicking the filter button. I expected to see {search:true, icon: 'times'} as context value. Thx!
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js
There is one important things to note here. useEffect in App.js is run once and hence the onClick function set with setNavigationState will use the values from its closure at the point at which it is defined i.e initial render.
Due to this, when you call the function within Header.js from context's the value along with the localState are being reset to the initial value.
SOLUTION 1:
One solution here is to use callback approach to state update. For that you need to modify your implementation on useObject a bit to provide the use the capability to use the callback value from setState
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
if(typeof newState === 'function') {
setState((prev) => ({ ...prev, ...newState(prev)}));
} else {
setState({
...state,
...newState
});
}
}
];
};
and then use it in onContextClick function like
const onContextClick = () => {
setState(prevState => {
setNavigationState(prev => ({ icon: ICON[prevState.isOpen ? 0 : 1] }));
return { isOpen: !prevState.isOpen };
});
};
Working DEMO
SOLUTION 2:
The other simpler approach to solving the problem is to use useCallback for onContextClick and update the navigation state with useEffect, everytime the closure state is updated like
const onContextClick = React.useCallback(() => {
setNavigationState({ icon: ICON[state.isOpen ? 0 : 1] });
setState({ isOpen: !state.isOpen });
}, [state]);
useEffect(() => {
setNavigationState({
search: true,
icon: ICON[0],
onClick: onContextClick
});
}, [onContextClick]);
Working demo
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] }))
}, [])