fetching data after useReducer dispatch changes local state - reactjs

Trying to rewrite this method using react hooks:
this.setState({
isPerformingAuthAction: true
}, () => {
auth.signInWithEmailAndPassword(emailAddress, password).then((value) => {
this.closeSignInDialog(() => {
const user = value.user;
const displayName = user.displayName;
const emailAddress = user.email;
this.openSnackbar(`Signed in as ${displayName || emailAddress}`);
});
}).catch((reason) => {
const code = reason.code;
const message = reason.message;
switch (code) {
case 'auth/invalid-email':
case 'auth/user-disabled':
case 'auth/user-not-found':
case 'auth/wrong-password':
this.openSnackbar(message);
return;
default:
this.openSnackbar(message);
return;
}
}).finally(() => {
this.setState({
isPerformingAuthAction: false
});
});
});
isPerformingAuthAction is a component property that disables buttons while performing various actions.
<Button disabled={state.isPerformingAuthAction} ... />
The Problem: I am using useReducer to manage local state and isPerformingAuthAction is a property of the reducerState. useReducer doesn't return a promise, so I can't do this:
dispatch({type:"isPerformingAuthAction", payload: true}).then(auth.stuff)
I thought about using useState instead, but it doesn't have a call back like this.setState with classes does.
I have been trying to wrap my head around some of the useEffect ways around fetching data and async. I am pretty confused about it, but I think I could have "isPerformingAuthAction" or the state from the reducer as a dependency and have useEffect update the component when it changes but the "isPerformingAuthAction" property changes for other reasons as well, not wtih just auth.signInWithEmailAndPassword(emailAddress, password), but signUp, signOut and a few others.
So when "isPerformingAuthAction" changes to true, it disables buttons so the user is forced to wait for the response from the server. I want to make sure those buttons are disabled (state updated/component re-rendered with greyed-out buttons) before I call the auth.whatever.

Related

React-Redux: how to set the state?

I am trying to understand someone else their code but have difficulty understand the interaction between Redux and React.
On a React page, I invoke a Redux action called getSubscriptionPlan. Inside that Redux action, I see it is able to load the correct data (point 1 below). This uses a reducer, in which I can again confirm the correct data is there (point 2 below).
Then the logic returns to the React page (point 3 below). I now would expect to be able to find somewhere in the Redux store the previously mentioned data. However, I can't find that data listed anywhere... not in this.state (where I would expect it), nor in this.props. Did the reducer perhaps not update the store state...?
What am I doing wrong and how can I get the data to point 3 below?
React page:
import { connect } from "react-redux";
import { getSubscriptionPlan } from "../../../appRedux/actions/planAction";
async componentDidMount() {
let { planId } = this.state;
await this.props.getSubscriptionPlan(planId);
// 3. I can't find the data anywhere here: not inside this.state and not inside this.props.
this.setState({plan: this.state.plan});
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.payment.paymentData !== this.props.payment.paymentData) {
this.setState({
checkout: this.props.payment.paymentData,
plan: this.props.payment.paymentData.plan,
});
}
}
const mapStateToProps = (state) => {
return {
plan: state.plan,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(
{ getSubscriptionPlan }, dispatch
);
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Checkout)
);
Redux action:
export const getSubscriptionPlan = (id) => {
let token = getAuthToken();
return (dispatch) => {
axios
.get(`${url}/getSubscriptionPlan/${id}`, {
headers: { Authorization: `${token}` },
})
.then((res) => {
if (res.status === 200) {
// 1. From console.log(res.data) I know res.data correctly now contains the data
return dispatch({
type: GET_PLAN_SUCCESS,
payload: res.data,
});
})
};
};
Reducer:
export default function planReducer(state = initial_state, action) {
switch (action.type) {
case GET_PLAN_SUCCESS:
// 2. I know action.payload, at this point contains the correct data.
return { ...state, plan: action.payload };
default:
return state;
}
}
You are getting tripped up on how Redux works.
Redux does not use react component state. It manages state separately, and passes that state to components as props. When you call getSubscriptionPlan, you asynchronously dispatch an event to Redux, which handles the event and updates store state in the reducer. This state is the passed to the connected components mapStateToProps function, mapped to props, and then passed as props to your component. Passing new props triggers a componentDidUpdate and a rerender of the component.
A few key things here.
Redux does not interact with component state unless you explicitly set state with props passed from Redux.
Redux is asynchronous. That means that when you make a change to state via dispatch, the change is not immediately available in the component, but only available when new props are passed. It's event driven, not data binding. As a result, in your code you woun't see the plan prop in componentDidMount because at the time componentDidMount the call to getSubscriptionPlan hasn't happened.
You should see the prop populated in this.props in componentDidUpdate and in render before the didUpdate.
When working with react, it's best to think of components as basically functions of props with some extra lifecycle methods attached.

Special actions on React state variables

I have a React component with a state variable that needs specific actions. For example, consider a component that shows a list of user profiles, and the user can switch to another profile or create a new one. The state variable is a list of user profiles, and a second variable is the currently selected profile; the component can add a new profile (which is more specific than just "setting" a new list of profiles), or it can change the currently selected profile.
My first idea was to have two useState hooks, one for the list and one for the current profile. However, one problem with that is that I would like to store the current profile's id, which refers to one of the profiles in the list, which means that the two state variables are inter-dependent. Another issue is that having a generic setProfiles state change function is a bit too "open" for my taste: the add logic may be very specific and I would like to encapsulate it.
So I came up with this solution: a custom hook managing the two state variables and their setters, that would expose the two values (list and current id) and their appropriate actions (add new profile and select profile).
This is the code of the hook:
export const useProfileData = () => {
const [ profiles, setProfiles ] = useState([]);
const [ currentProfileID, setCurrentProfileID ] = useState(null);
const [ currentProfile, setCurrentProfile ] = useState(null);
useEffect(() => {
// This is actually a lazy deferred data fetch, but I'm simplifying for the sake of brevity
setProfiles(DataManager.getProfiles() || [])
}, [])
useEffect(() => {
if (!profiles) {
setCurrentProfile(null);
return;
}
const cp = profiles.find(p => p.ID === currentProfileID);
setCurrentProfile(cp);
}, [ currentProfileID, profiles ])
return {
currentProfile: currentProfile,
profiles: profiles,
setCurrentProfileID: i_id => setCurrentProfileID(i_id),
addNewProfile: i_profile => {
profiles.push(i_profile);
setProfiles(profiles);
DataManager.addNewProfile(i_profile); // this could be fire-and-forget
},
};
};
Three states are used: the list, the current profile id and the current profile (as an object). The list is retrieved at mounting (the current id should be too, but I omitted that for brevity). The current profile is never set directly from the outside: the only way to change it is to change the id or the list, which is managed by the second useEffect. And the only way to change the id is through the exposed setCurrentProfileID function.
Adding a new profile is managed by an exposed addNewProfile function, that should add the new profile to the list in state, update the list in state, and add the new profile in the persistent DataManager.
My first question is: is it ok to design a hook like this? From a general software design point of view, this code gives encapsulation, separation of concerns, and a correct state management. What I'm not sure about if this is proper in a functional world like React.
My second question is: why is my component (that uses useProfileData) not updated when addNewProfile is called? For example:
const ProfileSelector = (props) => {
const [ newProfileName, setNewProfileName ] = useState('');
const { profiles, currentProfile, setCurrentProfileID, addNewProfile } = useProfileData();
function createNewProfile() {
addNewProfile({
name: newProfileName,
});
}
return (
<div>
<ProfilesList profiles={profiles} onProfileClick={pid => setCurrentProfileID(pid)} />
<div>
<input type="text" value={newProfileName} onChange={e => setNewProfileName(e.target.value)} />
<Button label="New profile" onPress={() => createNewProfile()} />
</div>
</div>
);
};
ProfilesList and Button are components defined elsewhere.
When I click on the Button, a new profile is added to the persistent DataManager, but profiles is not updated, and ProfilesList isn't either (of course).
I'm either implementing something wrong, or this is not a paradigm that can work in React. What can I do?
EDIT
As suggested by #thedude, I tried using a reducer. Here is the (stub) of my reducer:
const ProfilesReducer = (state, action) => {
const newState = state;
switch (action.type) {
case 'addNewProfile':
{
const newProfile = action.newProfile;
newState.profiles.push(newProfile);
DataManager.addNewProfile(newProfile);
}
break;
default:
throw new Error('Unexpected action type: ' + action.type);
}
return newState;
}
After I invoke it (profilesDispatch({ type: 'addNewProfile', newProfile: { name: 'Test' } });), no change in profilesState.profiles is detected - or at least, a render is never triggered, nor an effect. However, the call to DataManager has done its job and the new profile has been persisted.
You should never mutate your state, not even in a reducer function.
From the docs:
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Change your reducer to return a new object:
const ProfilesReducer = (state, action) => {
switch (action.type) {
case 'addNewProfile':
{
const newProfile = action.newProfile;
return {...state, profiles: [...state.profiles, newProfile]}
}
break;
default:
throw new Error('Unexpected action type: ' + action.type);
}
return state;
}
Also not that reducer should no have side effects, if you want to perform some action based on a state change, use a useEffect hook for that.
For example:
DataManager.addNewProfile(newProfile) should not be called from the reducer

Is it normal that the component gets rendered twice when two state variables are updated?

In my test app, every time a certain function is called, a call to my API is made (axios.get) and then two state variables are updated with the data received from the database. These two state variables both change a part of what is shown on the screen.
The things is, I added a useEffect hook to "debug" the amount of re-renders and I noticed that the component is re-rendered twice, I guess because it is once for one state variable and once for the other one. I thought using useReducer would change this, but it doesn't.
Is this a normal React behaviour or is there something I should be doing differently in order for the component is re-rendered only once?
Edit: I am editing to add the code:
(It's a trivia kind of test app, I'm new to React, so I am practicing)
import React, { useEffect, useReducer } from 'react'
import axios from 'axios'
import './App.css';
import reducer from './Reducer.js'
const initialState = {
question: '',
id: 1,
choices: []
}
const Questions = () => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
console.log('executed');
})
const getQuestion = async (e) => {
try {
e.preventDefault()
const res = await axios.get(`/questions/${state.id}`)
dispatch({
type: 'set_question',
payload:
res.data.question
})
dispatch({
type: 'set_choices',
payload:
[res.data.correct_answer,
res.data.incorrect_answer1,
res.data.incorrect_answer2]
})
} catch (err) {
console.log(err);
}
}
return (
<div>
<form onSubmit={getQuestion}>
<button>Get next question</button>
</form>
<h1> {state.question ? state.question : null}</h1>
<button> {state.choices ? `${state.choices[0]}` : null}</button>
<button> {state.choices ? ` ${state.choices[1]}` : null}</button>
<button> {state.choices ? ` ${state.choices[2]}` : null}</button>
</div>
)
}
export default Questions
Reducer:
const reducer = (state, action) => {
switch (action.type) {
case 'set_question':
return {
...state,
question: action.payload
}
case 'set_choices':
return {
...state,
choices: action.payload
}
default:
return state
}
}
export default reducer
React only batches state updates in event handlers and lifecycle methods. If the state updates happen in an async function e.g. in response of a successful call to fetch or a setTimeout they will not be batched. This is announced to change in a future version of react.
Also see this answer from Dan Abramov about this:
https://stackoverflow.com/a/48610973/5005177
However, both in React 16 and earlier versions, there is yet no batching by default outside of React event handlers. So if in your example we had an AJAX response handler instead of handleClick, each setState() would be processed immediately as it happens. In this case, yes, you would see an intermediate state.
promise.then(() => {
// We're not in an event handler, so these are flushed separately.
this.setState({a: true}); // Re-renders with {a: true, b: false }
this.setState({b: true}); // Re-renders with {a: true, b: true }
this.props.setParentState(); // Re-renders the parent
});
If you want your component to re-render only once you have to keep all the data in a single state object so that you only have to call setState once (or dispatch if you want to use useReducer) with the new data.
Another workaround is to wrap your block of state updates in ReactDOM.unstable_batchedUpdates(() => {...}), which will most likely not be required anymore in a future version of react. Also see the answer of Dan Abramov from above for details.

React.js issues with re-rendering when trying to show notifications in a context from Apollo useQuery

So this is my what I am trying to do. I have created a global notifications context with a reducer to change the state.
My issue case now is, I am using Apollo hooks to do a query useQuery and when there is an error I want to show it in my global notifications.
So to do this, I need to dispatch to the reducer to show the notification, change the title and the message in the notification.
This triggers a state change on the global notification context which triggers a rerender which triggers the useQuery again. All this triggers an infinite rerender if you have an error response every time.
Does someone have a suggestion to have a global notifications system that could work for this?
The only solution I have left is to import the notification on every page where I need a notification.
Code:
const [state, dispatch] = useContext(NotificationsContext);
const { loading, error, data } = useQuery(GET_USERS);
useEffect(() => {
if (error) {
dispatch({
type: 'ERROR',
payload: {
title: 'Query Error',
subtitle: error.message,
},
});
}
if (!loading && data) {
const newData = [];
data.getUsers.users.forEach((user) => {
const newUserRow = {};
headers.forEach((header) => newUserRow[header] = user[header]);
newData.push(newUserRow);
});
setRows(newData);
}
}, []);
I think you are using functional component and you can solve this by using useQuery inside the useEffect hook also do not forget to pass empty array as a 2nd argument of useEffect.
Hope this will solve your problem.
https://medium.com/#felippenardi/how-to-do-componentdidmount-with-react-hooks-553ba39d1571

React + Redux: proper way to handle after-dispatch logic

I have a component with some internal state (e.g. isLoading) which has access to redux data. In this component I'd like to dispatch a thunk action (api request) resulting in redux data change. After the thunk is completed I need to change the state of my component. As I see it, there are two ways to do so:
Use the promise return by the thunk and do all I need there, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id).then(() => this.setState({ isLoading: false }));
};
Pass a callback to the thunk and fire it from the thunk itself, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id, this.onSaveSuccess);
};
Which one is the correct way to do so?
The safer way is to use the promise implementation, as you'll be sure that function will only run after the promise has been resolved. The second implementation has no inherent flaws, but if anything in your thunk is async, then it will not work correctly since it'll run once the code is reached, not when the code above it finishes executing. When handling anything that can be async (server requests/loading data/submitting data), it's always safer to use Promise implementations.
Probably the best practice for updating component level state or running a callback function based on changing redux state (or any props/state changes for that matter) is to use componentDidUpdate or useEffect:
componentDidUpdate(prevProps, prevState){
if(prevProps.someReduxState !== this.props.someReduxState && this.state.isLoading){
setState({isLoading:false})
}
}
With useEffect:
useEffect(()=>{
if(!props.someReduxState){
setLoading(true)
} else {
setLoading(false)
}
},[props.someReduxState])
However, I might recommend a different approach (depending on the goal especially on initial data fetching) that manages the loading of state within redux:
Initialize your redux state with a loading value instead:
export default someReduxState = (state = {notLoaded:true}, action) => {
switch (action.type) {
case actions.FETCH_SOME_REDUX_STATE:
return action.payload;
default:
return state;
}
}
then in your component you can check:
if (this.props.someReduxState.notLoaded ){
// do something or return loading components
} else {
// do something else or return loaded components
}

Resources