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 })}
/>
Related
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.
I have a few inputs for which I'm getting values and setting to redux state and at onClick sending them to api.
Here are inputs and checkboxes
FormInput(label="Name" placeholder="" onChange=event => this.props.getValue(event.target.value, 'FIRST_NAME'))
FormInput(label="Last Name" placeholder="" onChange=event => this.props.getValue(event.target.value, 'LAST_NAME'))
CheckBox(label="A" onClick=event => this.props.getValue(event.target.checked, 'A'))
CheckBox(label="B" onClick=event => this.props.getValue(event.target.checked, 'B'))
At on change and onClick i'm passing values to redux state via one redux action.
For making things simple and updating redux state via just one onChange handler I used :
function mapDispatchToProps(dispatch) {
return {
getValue: bindActionCreators(getValue, dispatch),
};
}
It's my redux action.
It's getting values from all inputs and checkboxes and passing them to redux state.
Here is action type. I'm passing it like a parameter from inputs, this way I keep my action minimal, so, not using different actions for all inputs instead just using one handler function for all.
export const getValue = (value, type) => {
return dispatch => {
console.log(value)
dispatch({
type: 'SET_' + type,
payload: value
});
};
};
and here's my reducer:
import {
SET_FIRST_NAME,
SET_LAST_NAME,
SET_A,
SET_B
} from "Store/Types/Form/";
let initialState = {
firsName: null,
lastName: null,
options: [
{
id: 1,
name: "selection A",
selection: false,
},
{
id: 1,
name: "selection B",
selection: false,
}
]
};
export default (state = initialState, action) => {
switch (action.type) {
case SET_FIRST_NAME:
return { ...state, firsName: action.payload };
case SET_LAST_NAME:
return { ...state, lastName: action.payload };
default:
return state;
}
};
In my initial state options array I want to pass values from checkboxes so SET_A and SET_B and that's how I want my object to look.
How I can pass my values how I want and how can I prevent duplicating if I use push?
If everything works I will just pass my redux state to app directly.
So you shouldn't push to state directly since it's supposed to be immutable. but you can use produce from immer to clone state. This was you can change the cloned object and reassign state object
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.
Cannot work out what is going on here but basically i have a json file which has a load of products. im trying to then render the ones I want
here is my reducer:
export default(state = initialState, action) => {
switch(action.type){
case Types.SHOW_PRODUCTS: {
console.log('here1');
let productsToShow = data.filter(category => category.link === action.category)
const newState = [].concat(productsToShow[0].products)
return newState;
}
default:
console.log('here2');
return state;
}
}
when I log the state in my store, it says that productsToRender is an array of length 5 (this is correct)
however, when I log (this.props) in one of my components, it says that the length is 0
in the above reducer, the console.log('here 1') is the last console log being called, meaning that it is definitely returning products ( that is verified in the store state). so im not sure why it is then wiping it in that component?
in that component I call this
componentWillMount = () => {
this.props.showProducts(this.props.chosenCategory.category)
}
which passes in the chosen category so I now what products to render
however, logging this.props in the render method below, is showing it to be an empty array
of course I can post more code if necessary but any reason for this funky behaviour?
extra info:
interestingly when I do this:
default:
console.log('here2');
return [{name: 'prod'}];
}
and then log this.props, it now contains this array with this object???
The store should be immutable, that is, the value you return should be made immutable.
I assume you are adding only a single array in the store
Try changing the reducer like,
const initialState = [];
export default(state = initialState, action) => {
switch(action.type){
case Types.SHOW_PRODUCTS: {
console.log('here1');
let productsToShow = data.filter(category => category.link === action.category)
let newState = [...state,...productsToShow[0].products]
return newState;
}
default:
console.log('here2');
return state;
}
}
I'm trying to implement a method to store in Redux session the counting of results from the base everytime the application also fetches it. The componentWillReceiveProps method is the following:
if (!countingTestAnnouncements && countingTestAnnouncementsSuccess) {
let value = parseInt(totalTests.total);
setCurrentValue(value);
}
It is clear. The method to store in the session will be executed when the counting is successful. This is the action file:
export const SET_CURRENT_VALUE = "hp:alert:set_current_value";
export function setCurrentValue(currentValue) {
return (dispatch) => {
dispatch({
type: SET_CURRENT_VALUE,
payload: currentValue
})
};
}
const ACTION_HANDLERS = {
[SET_CURRENT_VALUE]: (state, action) => {
return {
...state,
currentValue: Value, action.payload
}
}
};
const initialState = {
currentValue: null
};
export default function alertReducer (state = initialState, action) {
const handler = ACTION_HANDLERS[action.type];
return handler ? handler(state, action) : state
};
What is causing me a headache is the fact that when the return is reached within ACTION_HANDLERS, the looping will occur, and I do not know why it is happening.
const ACTION_HANDLERS = {
[SET_CURRENT_VALUE]: (state, action) => {
// By commenting out the return block and putting
// a console.log, the result will be seen only once,
// as expected. But as it is, the loop will happen.
return {
...state,
currentValue: action.payload
}
// console.log(action.payload) will display once the counting.
}
};
Why don't know why componentWillReceiveProps keeps repeating like this. What is really pissing me off is that the whole block is executed, the conditions of the if should be false and do not enter the block.
Whenever you are calling "setCurrentValue(value)" you are triggering and "componentWillReceiveProps" after the dispatch and this cause the loop.
It might be better to get this total in reducer and pass it to store or to reset countingTestAnnouncementsSuccess in order to skip it on next update like given below:
if (!countingTestAnnouncements && countingTestAnnouncementsSuccess) {
let value = parseInt(totalTests.total);
countingTestAnnouncementsSuccess = false;
setCurrentValue(value);
}