How to handle, after state change in React context dispatch - javascript

I have a react version 16 application. The state changes are handled using React's Context, reducer concept.
To change the state and do something post that change can be handled as given below. For Example - I am in component 'Contact'. Inside this I am dispatching a state change on a variable age which is a number as given below -
dispatch({
type: "CHANGE_AGE",
payload: age
})
and catch the dispatch in the reducer as given below
case "CHANGE_AGE":
return {
...state,
age: action.payload
}
To do something in 'Contact' component after there is a change in age, we can use 'useEffect' to watch for change in 'age'
useEffect({
//doSomething()
},[state.age])
The problem is - What if we had to change an object in state, ie instead of changing the age, we had to change the address like given below and we had to do something after address has changed.
How do we watch for a change in address?
dispatch({
type: "CHANGE_ADDRESS",
payload: {
city: "Delhi",
country: "India",
zip: '22233'
}
})
One probable solution - Along with address dispatch, we can dispatch a variable, on change of which we can do something. But this is not a perfect solution.

Just store both: the previous and the current addresses
// your-state.js
const initialState = {
age: 0,
prevAdress: {
city: '',
country: '',
zip: ''
},
currAdress: {
city: '',
country: '',
zip: ''
},
...
}
and when you dispatch, write current as previous in a reducer
// your-reducer.js
...
case "CHANGE_ADDRESS":
return {
...state,
prevAddress: { ...state.currAddress },
currAddress: { ...action.payload }
}
Then compare in the effect
// your-component.js
...
useEffect(() => {
if (state.prevAddress.zip !== state.currAddress.zip) { // just an example of comparison
// doSmthg
}
}, [state]);
...
UPDATE
If you need to compare address only once and do something based on that comparison then just compare before dispatching a value:
// your-component.js
...
useEffect(() => {
const doDispatch = () => dispatch({
type: "CHANGE_ADDRESS",
payload: {
city: "Delhi",
country: "India",
zip: newZip,
},
});
if (state.address.zip !== newZip) {
doSmthgAsPromise().then(() => { // do something and then dispatch
doDispatch();
});
} else {
doDispatch();
}
}, [state, dispatch, doSmthgAsPromise]);

Related

Redux Toolkit caseReducers skip prepare callback in reducer function

I have a reducer for adding comment as follow
addComment: {
reducer: (state,action)=>{
const { newId, comment } = action.payload
console.log(newId)
// functions that create an new comment entry with the newID and update state
},
prepare: (input) =>{
return {
payload: {
...input,
newId: nanoid(),
}
}
}
If I call dispatch within my app as follow, the console.log shows the id generated by nanoid(). So far so good.
dispatch(addComment({comment: comment}))
However if I call the reducer within another reducer using the caseReducers, console.log returns undefined
commentSlice.caseReducers.addComment(state,{
payload: {
comment : comment
}
})
How do I get the prepare callback to run when using caseReducers.
Based on the docs it looks like if you are to invoke using the caseReducer way, you need to include the "type":
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
// now available:
slice.actions.increment(2)
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })

Better way to empty all the values of an object by setState in react

I am using React. This is my state
state = {
customerData: {
id: '',
name: '',
type: '',
place: '',
country: '',
timezone: 'GMT+5:30',
status: false
}
}
There is an edit functionality where the customerData object gets populated on click of edit button. I am showing this data in a modal.
Now in the modal, when I click the submit button, the modal should hide and the data populated in the customerData object should be empty. I can do it like this:
this.setState({
customerData: {
...this.state.customerData
id: '',
name: '',
type: '',
place: '',
country: '',
timezone: '',
status: false
}
}
})
I am wondering is there any one line way in ES6 to make the customerData Object empty. Because, if there are too many fields, it will be difficult.
There are two easy options here:
Create a default object
Above your component you can create the 'empty' value for your state. For example:
const emptyCustomerData = {
id: '',
name: '',
type: '',
place: '',
country: '',
timezone: '',
status: false,
}
When you want to clear your state object, now you just call:
this.setState({
customerData: emptyCustomerData,
})
Allow all the values of customerData to be nullable
You could simply set customerData to an empty object:
this.setState({
customerData: {},
})
Doing this means that before using any of the properties in your code, you need to check for undefined:
// This sets myVal to customerData.name, or if name is undefined, to an empty string
const myVal = this.state.customerData.name || ''
You can set a variable like initialState:
const initialState = {
customerData: {
id: '',
name: '',
type: '',
place: '',
country: '',
timezone: 'GMT+5:30',
status: false
}
}
And before you hide the modal, do a:
this.setState(initialState)
Assuming you want to preserve the customerData keys, for your use case it may be sufficient to set everything to null which would simplify the reset:
this.setState(customerData: Object.assign(...Object.keys(this.state.customerData).map(k => ({[k]: null}))))
https://jsfiddle.net/0ymx7fsq/
Alternatively, since you're using class components you can also save the initial state in a constructor as well:
constructor(props) {
super(props)
this.state = initialState;
}
...
reset() {
this.setState({ customerData : initialState.customerData });
}
...
You can use reduce in order not to rely on params count and number. It should looks like this
const data = {
id: '1',
name: '2',
type: '3',
place: '4',
country: '5',
timezone: '6',
status: true
}
function getDefaultObject(obj) {
const defaultValues = {
'string': '',
'boolean': false
}
return Object.keys(obj).reduce((acc, rec, index) => {
return { ...acc, [rec]: defaultValues[typeof obj[rec]]}
}, {})
}
console.log(getDefaultObject(data))
Just facing your problem today and i solved it with this code
const handleResetState = () => {
const {customerData} = this.state
let emptyState = {};
for (const key in customerData){
emptyState = {...emptyState, [key] : ""}
}
//i think you will more need the second option, so dont use first option
//normal change (first option)
this.setState({customerData: emptyState});
//in case if your key need to have special initial value that are not empty string (second option)
this.setState({customerData: {...emptyState, status: false} })
}

useReducer React approach

I'm playing with useReducer react's api, and wonder to know the difference between the theoretical (documentation) part and one I implement.
intialState of commponent with useReducer hook:
const [fields, dispatch] = React.useReducer(formReducer, {
firstName: { value: '', isValid: false },
lastName: { value: '', isValid: false },
});
theoretical Variant
const formActionTypes = {
firstName: 'FIRST_NAME',
lastName: 'LAST_NAME',
};
....
function formReducer(state, action) {
switch (action.type) {
case formActionTypes.firstName:
return { ...state, firstName: { ...action.payload } };
case formActionTypes.lastName:
return { ...state, lastName: { ...action.payload } };
default:
return state;
}
}
....
dispatch({
type: formActionTypes[name], //name is input name
payload: { value, isValid } //value is e.target.value
});
MY implimentation
function formReducer(state, action) {
return { ...state, [action.name]: { ...action.payload } };
}
....
dispatch({
name, //name is input name
payload: { value, isValid } //value is e.target.value
});
The two reducers you've shown will both work and produce identical results if that's what you're asking. I think the theoretical version version you're getting from the documentation is meant to demonstrate a particular concept, though, which your reducer arguably violates (though it's not a big deal; our job is to make working code, not to pass some purity test!).
Specifically, you typically want to somewhat decouple actions from state. The action shouldn't just be a mirror of your state data structure; if you want that coupling, you'd might as well use useState instead and just set the state directly. A reducer is meant to decouple this by you modeling a description of the action, and then it's only the reducer that decides how that action acts on state. You might, for example, decide to add a clear form button. With your current pattern, you'd have to dispatch two actions, which would cause two state updates, because your actions closely model the state. The switch statement pattern allows you to easily apply different types of logic based on different types of actions.
There are no wrong answers, just different approaches all with their merits. Here's one that I think introduces better decoupling by letting the reducer logic take care of knowing about whether a field is valid:
const SET_FIRST_NAME = Symbol();
const SET_LAST_NAME = Symbol();
const CLEAR_FORM = Symbol();
// Call action creators instead, like: dispatch(setFirstName(e.target.value));
const setFirstName = name => { type: SET_FIRST_NAME, value: name };
const setLastName = name => { type: SET_LAST_NAME, value: name };
const clearForm = () => { type: CLEAR_FORM };
const initialState = {
firstName: { value: '', isValid: false },
lastName: { value: '', isValid: false }
};
const notEmpty = value => !!(value && value.trim().length);
const validateFirstName = notEmpty; // Or replace with different logic
const validateLastName = notEmpty;
const reducer = (state, action) => {
switch (action.type) {
case SET_FIRST_NAME:
return {
...state,
firstName: {
value: action.value,
isValid: validateFirstName(value)
}
}
case SET_LAST_NAME:
return {
...state,
lastName: {
value: action.value,
isValid: validateLastName(value)
}
}
case CLEAR_FORM:
return initialState;
default:
return state;
}
};

react redux merge state with given array

Lets say I have a reducer which is like :
const initialState = [
{
accessToken: null,
isLoggedIn: false,
}
]
export default function my_reducer(state = initialState, action) {
switch (action.type) {
case LOGIN:
return state.merge(user: action) ---> how to handle this
and the output should be like:
[
{
accessToken: null,
isLoggedIn: false,
user: {
name: 'some name',
email: 'some email'
}
]
In action I am getting a array which I am providing by doing JSON.stringify(response)
previous data should not be changed and new data should be updated
The ES6 Way
To create a new object with the state in ES6 we can use the spread operator. Like this
...
case ActionType.SUCCESS_GET_DATA : {
let newState = { ...state, [action.uniqueKey]: action.payload };
return state.merge(newState);
}
...
I did the uniqueKey part as a variable because you will want a unique name for your state.
IMO this syntax is much easier to understand than the Object.assign
You can use Object.assign() function:
var state = {
accessToken: null,
isLoggedIn: false,
};
var user = {
name: 'some name',
email: 'some email'
};
var newState = Object.assign({}, state, {user});
console.log(newState);
First I see that your state is actually an array, but I think you would need an object right?
So it would be:
const initialState = {
accessToken: null,
isLoggedIn: false,
}
(requires Babel) So with spread operator you can:
return {
...initialState,
user: {
name: '...',
surname: '...'
}
};
Or if you do not transpile via Babel alike:
return Object.assign({}, initialState, {
user: {
name: '...',
surname: '...'
}
});
Using ES6 spread syntax
...
case 'ACTION_TYPE_A': {
return { ...state, action.key: action.value };
}
...
This will return the merged state by updating the 'key' if it exists in the original state.
Everything according to new Es6 format : )
A total addToDo reducer function where the data is appended to the previous state. And you get the output of a new state data : )
export const addToDo = (state, action) => {
const { name, email, phone, image,key } = action;
var data = [...state.data];
var newData = {
name: name, email: email, phone: phone, image: image,key:key
}
data.push(newData)
return(
state.merge({
data : data
})
)};
Happy coding.

redux dispatch action and reducer with new state

I have actions like:
export function login(){
my_login(function(response){
if(response.status == "ok"){
loginSuccess(response)
}
})
}
export function loginSuccess(response){
return dispatch => {
dispatch({ response, type: types.LOGIN });
console.log("dispatched");
console.log(getState()); ---> I want the updated state
router.transitionTo("/some_url");----> after this I want to route it to somewhere
};
}
When my login action is called it again calls my_login and then I am dispatching my loginSuccess action.
I have a reducer like:
const initialState = [
{
fname: null,
lname: false,
}
]
export default function login(state = initialState, action) {
switch (action.type) {
case LOGIN:
console.log("actions dude")
console.log(action)
return
[{
fname: action.fname,
lname: actin.lname,
}]
default:
return state
}
}
Here my state is not changing and I am not getting the getState() value in action above.
I dont think my reducer is called and state is updated.
Can anyone sugges me whats wrong in here ?
I think you are missing dispatch(loginSuccess(response))
Beware of the bare return statement. One of the known pitfalls of automatic semicolon insertion is that return becomes return;.
So instead of:
return
[{
fname: action.fname,
lname: action.lname,
}]
Do this instead:
return [{
fname: action.fname,
lname: action.lname,
}];

Categories