Issues with successive state object setting in reactjs hooks - javascript

I am starting to using react hooks and i decide to put an object instead of a variable in the useState function :
const [foo, setFoo] = useState({
term: '',
loading: false,
...
});
but later when I want to update it
const onChange = (e) => {
const { value } = e.target;
setFoo({ ...foo, term: value });
if(value) setFoo({ ...foo, loading: true });
...
}
...
return (
...
<input onChange={onChange} value={term} />
...
)
1. Why in the second setFoo when I check the foo object I get alway term property equal to '' exactly like the initial value and the input don't get updated with the typed characters ?
- When I delete the second setFoo it works so I guess because setFoo is asynchronous but how to solve this issue ?.
- I know that we can workaround this issue by managing to call setFoo just once but i want to know other solutions ?
2. Why this kinda of issues never happened in redux?

The solution: use one setFoo like this:
const onChange = (e) => {
const { value } = e.target;
setFoo({ term: value, loading: !!value });
...
}
!! means "convert to boolean value".

1. Probably because you have to destruct foo instead of state
setFoo({ ...foo, term: value });
working useState example
2. Take a look at the additional hooks, especially useReducer
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
And your code can become:
const reducer = (state, action) {
switch(action.type) {
case 'loading':
return { ...state, loading: true };
case 'loaded':
default:
return { ...state, loading: false };
}
}
const initialState = {
term: '',
loading: false,
}
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({
type: e.target.value ? 'loaded' : 'loading'
});
working useReducer example on CodeSandbox

You can solve the issue by calling setFoo only once with all the new key-value pairs, like this:
const onChange = (e) => {
const { value } = e.target;
const loading = value ? true : false;
setFoo({ ...state, term: value, loading });
....
}
Update:
For complex state update and more control on that part, you can use useReducer, and write a separate reducer to update the state.

Related

Is it possible to dispatch multiple actions?

I'm trying to clear fields from a form (clearing state from the store) when the user changes their country so I was wondering if it was possible to dispatch two actions under one event... -- tho my action also doesn't clear the fields so not sure where I'm going wrong
in index.jsx
export default function Form() {
const {
apartmentNumber,
birthDay,
birthMonth,
birthYear,
buildingNumber,
countryCode
} = state;
const [formData, setFormData] = useState({
apartmentNumber,
birthDay,
birthMonth,
birthYear,
buildingNumber
});
const onInputChange = (attribute, value) => {
setFormData({
...formData,
[attribute] : value
});
};
const onCountryChange = (value) => {
dispatch(updateCountry(value));
dispatch(clearForm(formData));
};
in reducer.js --
export const initialState = {
apartmentNumber : '',
birthDay : '',
birthMonth : '',
birthYear : '',
buildingNumber : ''
};
export default (state, action) => {
const { payload, type } = action;
switch (type) {
case UPDATE_COUNTRY:
return {
...state,
countryCode : payload
};
case UPDATE_FIELDS: {
return {
apartmentNumber : initialState.apartmentNumber,
birthDay : initialState.birthDay,
birthMonth : initialState.birthMonth,
birthYear : initialState.birthYear,
buildingNumber : initialState.buildingNumber
};
}
default:
return state;
}
};
You can reset the values be passing in initialState. Ideally, you should have an action for when UPDATE_COUNTRY is successful. Then you can reset to initialState once the country has been successfully updated.
case UPDATE_COUNTRY_SUCCESS:
return initialState;
or if you don't want to add a success action, you can just do
case UPDATE_COUNTRY:
return {
...initialState,
countryCode: payload
};
As for dispatching multiple actions. You can use redux or if your reducer does not do a side effect you can change your reducer to handle these at one go.
See:
Sending multiple actions with useReducers dispatch function?
If your output is not side-effecty you can do similar to:
https://codezup.com/how-to-combine-multiple-reducers-in-react-hooks-usereducer/
I don't think you need to use context api for that just pass the reducer and state to the components you want to call dispatch from.
If they have side effects you can achieve chain the effects by setting a reducer which returns the next effect to be ran from useEffect this is useful if you need the ui to change in each disptach. For multiple effects you can combine them and make then all run in one useEffect.
Other than that libs like redux handle these basically out of the box. But I have never used redux.
You can do this using useReducer React hook. Take note of the createReducer function and how it can be composed to handle arrays of actions.
const createReducer = (actions, state) {
return (state, action) => {
if(Array.isArray(action)) {
actions.forEach(action => {
state = actions[action[i].type](state, action[i])
})
return state
} else if(actions[action.type]) {
return actions[action.type](state, action)
} else {
return state
}
}
}
const actions = {
INCREMENT: (state, action) => {
state.counter++
return state
}
}
const initState = () => ({
counter: 0
})
const reducer = createReducer(actions)
const App = () => {
const [state, setState] = React.useReducer(reducer, initState())
return <div>Count: {state.count}
<button onClick={e => setState([
{type: 'INCREMENT'},
{type: 'INCREMENT'}
])}>+</button>
</div>
}
I suspect your issue is that state is propagating through the DOM tree with every actions dispatched, which can lead to broken or weird DOM states. With this architecture, you apply each of the actions in the array before the state is returned, meaning propagation only occurs after all actions have been applied to the state.

Async/Await seems to have different behaviour in class methods and function expressions (React Hooks, Redux-Thunk)

I'm migrating a class-based react system to hooks, and I'm facing some challenges which I can't understand.
Take a look at the snippet below:
async onSearchforOptions(elementId) {
await this.props.onFetchOperatingSystems()
//(3) [{…}, {…}, {…}]
console.log(this.props.operatingSystems)
}
In this method, I am dispatching an action to update the redux state, and right after this I'm logging the result to make sure the information was fetched and updated in the redux state.
The problem is that in an application which uses functional components, the result doesn't seem the same. Instead of updating the redux state and recovering the info right after, it simply doesn't seem to update the state, even if I'm using "await" and the very same actions and reducers the class component is using:
const onSearchforOptions = async (elementId) => {
await props.onFetchOperatingSystems()
//[]
console.log(props.operatingSystems)
}
My connection for both components (the class component and the functional component):
const mapStateToProps = state => {
return {
operatingSystems: state.operatingSystemReducer.operatingSystems
}
}
const mapDispathToProps = dispatch => {
return {
onFetchOperatingSystems: () => dispatch(actions.fetchOperatingSystems())
}
}
export default connect(mapStateToProps, mapDispathToProps)(productsForm)
My actions:
export const fetchOperatingSystemsStart = () => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_START
}
}
export const fetchOperatingSystemsFail = (error) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_FAIL,
error: error
}
}
export const fetchOperatingSystemsSuccess = (operatingSystems) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS,
operatingSystems: operatingSystems
}
}
export const fetchOperatingSystems = () => {
return dispatch => {
dispatch(fetchOperatingSystemsStart())
return axios.get(url)
.then(response => {
const fetchedData = []
for (let key in response.data) {
fetchedData.push({
...response.data[key],
id: response.data[key].id
})
}
dispatch(fetchOperatingSystemsSuccess(fetchedData))
})
.catch(error => {
if (error.response !== undefined) dispatch(fetchOperatingSystemsFail(error.response.data))
else dispatch(fetchOperatingSystemsFail(error))
})
}
}
My Reducer:
const initialState = {
operatingSystems: [],
loading: false
}
const fetchOperatingSystemsStart = (state) => {
return updateObject(state, { loading: true })
}
const fetchOperatingSystemsSuccess = (state, action) => {
return updateObject(state, { operatingSystems: action.operatingSystems, loading: false })
}
const fetchOperatingSystemsFail = (state) => {
return updateObject(state, { loading: false })
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_OPERATING_SYSTEMS_START: return fetchOperatingSystemsStart(state)
case actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS: return fetchOperatingSystemsSuccess(state, action)
case actionTypes.FETCH_OPERATING_SYSTEMS_FAIL: return fetchOperatingSystemsFail(state)
default: return state
}
}
export default reducer
updateObject function:
export const updateObject = (oldObject, updatedProperties) => {
const element = {
// The values of the object oldObject are being spread, at the same time the values of
// updatedProperties are (I'm taking out the attributes of both objects with the spread operator).
// In this case, since the names of the attributes are the same,
// the attributes (which were spread) of the first object will have their values replaced
// by the values of the second object's attributes.
...oldObject,
...updatedProperties
}
return element
}
My Goal:
Accoding to the snippet below, my goal is to dynamically search for options and update it in my form, which is in the component state.
const onSearchforOptions = async (elementId) => {
let elementUpdated
switch (elementId) {
case 'operatingSystem': {
await props.onFetchOperatingSystems()
console.log(props.operatingSystems)
elementUpdated = {
'operatingSystem': updateObject(productsForm['operatingSystem'], {
selectValue: {
value: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.id : undefined,
label: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.name : undefined
},
elementConfig: updateObject(productsForm['operatingSystem'].elementConfig, {
options: props.operatingSystems
})
})
}
break
}
case 'productType': {
await props.onFetchProductTypes()
elementUpdated = {
'productType': updateObject(productsForm['productType'], {
selectValue: {
value: props.selectedElement.productType ? props.selectedElement.productType.id : undefined,
label: props.selectedElement.productType ? props.selectedElement.productType.name : undefined
},
elementConfig: updateObject(productsForm['productType'].elementConfig, {
options: props.productTypes
})
})
}
break
}
default: break
}
const productsFormUpdated = updateObject(productsForm, elementUpdated)
setProductsForm(productsFormUpdated)
}
The props object passed to the render function initially is not going to be mutated; rather the props passed to your component on its next render will be updated. This is more in keeping with the flux architecture. You fire-and-forget an action, the reducer runs, and then your component is re-rendered with new props.
Before, this same thing was happening, but the new props were being assigned to this.props again. Since there's no meaningful "this" anymore, you can't use this pattern. Besides, depending on this behavior is not idiomatically the React way of doing things.
Update:
I think this is like a great number of cases I've also encountered where the React team seemed to overcorrect for a lot of use cases of people handling derived state poorly (see You Probably Don't Need Derived State). I've seen plenty of cases, like yours, where the now-deprecated componentWillReceiveProps lifecycle method solved this problem for class-based components very nicely.
Thankfully, useEffect now gives you something like a replacement. Think about it this way: when props.operatingSystems changes, you want to perform the effect of changing the state of your form. It's an unfortunate double update issue, but you had that before. Here's how you could go about writing that:
const [productsForm, setProductsForm] = useState(...);
useEffect(() => {
// Handle the case where props.operatingSystems isn't initialized?
if (!props.operatingSystems || !props.selectedElement.operatingSystem)
return;
setProductsForm({
...productsForm,
operatingSystem: {
...productsForm.operatingSystem,
selectValue: {
value: props.selectedElement.operatingSystem.id,
label: props.selectedElement.operatingSystem.name
},
elementConfig: {
...productsForm.operatingSystem.elementConfig,
options: props.operatingSystems
}
}
});
}, [props.operatingSystems]);
The way this works is that your effect code is only kicked off whenever your props.operatingSystems value changes since the last render. You can do a similar sort of effect for product types.
Another option which is maybe less elegant is for your async function that kicked off the redux actions to also resolve to a value which you can then use in your state setting code:
const operatingSystems = await props.onFetchOperatingSystems();
// ...now set your state
i usually implements thunks in a functional component like:
`export default connect(mapStateToProps, {fetchOperatingSystems})(productsForm)`
can you try this and comment back.

What is the best or most efficient method for removing keys when setting state via React's useState hook?

Our project is embracing the new functional React components and making heavy use of the various hooks, including useState.
Unlike a React Class's setState() method, the setter returned by useState() fully replaces the state instead of merging.
When the state is a map and I need to remove a key I clone the existing state, delete the key, then set the new state (as shown below)
[errors, setErrors] = useState({})
...
const onChange = (id, validate) => {
const result = validate(val);
if (!result.valid) {
setErrors({
...errors,
[fieldId]: result.message
})
}
else {
const newErrors = {...errors};
delete newErrors[id];
setErrors(newErrors);
}
Is there a better alternative (better being more efficient and/or standard)?
If you need more control when setting a state via hooks, look at the useReducer hook.
This hook behaves like a reducer in redux - a function that receives the current state, and an action, and transforms the current state according to the action to create a new state.
Example (not tested):
const reducer = (state, { type, payload }) => {
switch(type) {
case 'addError':
return { ...state, ...payload };
case 'removeError':
const { [payload.id]: _, ...newState };
return newState;
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, {});
...
const onChange = (id, validate) => {
const result = validate(val);
if (!result.valid) {
dispatch({ type: 'addError', payload: { [id]: result.message }})
}
else {
dispatch({ type: 'removeError', payload: id })
}

React Native Redux don't change state I don't understand why

I'm using redux and redux-thunk long time, Im trying now this simple workflow but not working the my expected value
actioncreator =>
export const openguidelist = () => {
return dispatch => {
dispatch({ type: OPEN_GUIDE_LIST });
};
};
My reducer =>
const INITIAL_STATE = {
guideopen: true
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case OPEN_GUIDE_LIST:
return { ...state, guideopen: true ? false : true };
default:
return state;
}
};
and triggered actioncreator the my component onPress is nothing wrong, by the way Im looking react-native-debugger, initial work is working change the guideopen true to false my expected then guideopen is never change always return false,what I'm expecting is the change in value each time the onpressing triggers but redux don't just change the state first time and than nothing change the guideopen return always the same value (false) I don't understand why please explain me
true ? false : true will always evaluate to false.
It should be guideopen: !state.guideopen.
Also, if guideopen is the only state in the reducer, you can remove the nesting and use the boolean directly as the state:
(state = false, action) {
...
case OPEN_GUIDE_LIST:
return !state;

Redux previous state already has new state value

I'm trying to get to grips with controlled forms using React & Redux, and I've got it working so that when I'm typing in the input field the state is updating and passing down to the input component as intended.
However, in my reducer, when I console log the previous state, the form field's value doesn't contain the value from before the new character was typed, it already has the new character.
My reducer:
import initialState from '../state/form'
const form = (prevState = initialState, action) => {
switch (action.type) {
case 'INPUT': {
console.log(prevState) // the value here equals "test"
debugger // the value here equals "tes"
let newFields = prevState.fields
newFields[action.field].value = action.value
return Object.assign({}, prevState, {
fields: newFields
})
}
default: {
return prevState
}
}
}
export default form
If my input field contains the text "tes", I can then add a "t" and the action is dispatched as intended, but when it gets to this reducer, I console log the previous state and the field's value is "test", not "tes".
I'm expecting the previous state to have "tes", and the reducer to return the new state with "test".
In my container I have:
const dispatchToProps = (dispatch, ownProps) => {
return {
control: (e) => {
dispatch({
type: 'INPUT',
form: ownProps.formId,
field: e.target.getAttribute('name'),
value: e.target.value
})
},
clear: () => {
dispatch({
type: 'CLEAR_FORM',
form: ownProps.formId
})
}
}
}
So my input component is being passed the 'control' function. I've since used a debugger statement right next to the console.log in the reducer code above, and using Chrome's dev tools, this show prevState to have exactly what I expected (tes, not test). The console.log is still logging "test" though!
So it appears my redux implementation may be ok, there's just some voodoo somewhere as console.log(prevState) == "test" and the debugger allows me to watch the prevState variable and shows that it equals "tes", as expected!
Thanks for your answer #Pineda. When looking into bizarre console log behaviour (as you were typing your answer) I came across the variables are references to objects fact (here) - I've stopped mutating my state, and updated my reducer:
import initialState from '../state/form'
const form = (state = initialState, action) => {
switch (action.type) {
case 'INPUT': {
return Object.assign({}, state, {
fields: {
...state.fields,
[action.field]: {
...state.fields[action.field],
value: action.value
}
}
})
}
default: {
return state
}
}
}
and now it's all working correctly. I may have been appearing to get away with mutating state due to errors in my mapStateToProps method, which had to be resolved for the new reducer to work correctly.
You are mutating state in these lines:
let newFields = prevState.fields
newFields[action.field].value = action.value
// it's also worth noting that you're trying to access a 'value'
// property on newFields[action.field], which doesn't look
// like it'll exist
Which can be re-written as:
prevState.fields[action.field] = action value
You then use your mutated state to create a new object.
Solution:
import initialState from '../state/form'
const form = (prevState = initialState, action) => {
switch (action.type) {
case 'INPUT': {
console.log(prevState);
// You create a new un-mutated state here
// With a property 'fields' which is an object
// with property name whose value is action.field
// which has the value action.value assigned to it
const newState = Object.assign({}, prevState, {
fields: { [action.field]: action.value}
});
return
}
default: {
return prevState
}
}
}
export default form
I'm guessing you are binding your input directly to your redux store attribute:
<input
value={this.props.name}
onChange={e => this.props.name = e.target.value}
/>
Remember, values are passed by reference and not by value, if you modify your store value directly then when the action fires you will have already mutated your redux store state (and this is a big no no)
My suggestion is, try to find how are you passing this state around in your codebase, you should have something like:
<input
value={this.props.name}
onChange={e => dispatch({type: 'INPUT', field: 'name', value: e.target.value })}
/>

Categories