I have a problem with redux re-render. Or rather lack of it, because using useState, everything works perfectly. My question is - how should I use the selector, so once the data is fetched, the component is re-rendered. Also, with redux approach sometimes the object X returns an error of undefined - because the assigned data is not yet fetched. How to overcome this? Should I use useSelector in the useState? I rather use redux than the local state in this case.], but redux gives me this issue that I am not sure how to overcome.
So I am fetching my data in the useEffect hook. This should also update the redux' state (which it does).
useEffect(() => {
FetchService.fetch()
.then((res) => dispatch(fetch(res)))
.catch((err) => console.log(err));
}, []);
Then I am grabbing the state using a right selector:
const data = useSelector(selectData)
At the very end I am assigning this to the object (I cannot skip this, cuz I need it), but sometimes I get an undefined error already here, cuz the data is an empty array):
const DATA_TAB_DATASOURCES = {
dataX: [data],
};
In the component itself I check conditionally if the array isn't undefined, but it seems the problem comes from the selector as with the useState it works perfectly as the re-render is automatically triggered with the change.
{data && DATA_TAB_DATASOURCES[dataX].map((c: any, i: number) =>
.....
})}
EDIT:
Reducer:
const INITIAL_STATE: any = {
data: []
};
export const reducer = (state: any = INITIAL_STATE, action: any) => {
switch (action.type) {
case FETCH: {
return {
...state,
data: action.payload,
};
}
default:
return state;
}
};
export default reducer;
selector:
const globalSelector = (state: any) => state
export const selectData = createSelector(globalSelector, (data) => {
return data
});
Related
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.
I try to render items that are in my firestore database. It displays correctly on the first render but when I switch between tabs/navigations it will readd the data to the list again. If I keep on switching between the tabs it will add more and more.
When I use local state there is no issue with that but the issue is with when I use dispatch to display items it will readd more items to the list(not the firestore database).
useEffect(
() =>
onSnapshot(collection(db, "users"), (snapshot) => {
console.log(
"snapshot",
snapshot.docs.map((doc) => doc.data())
);
const firebaseData = snapshot.docs.map((doc) => doc.data());
dispatch(addItem(firebaseData));
setLocalState(firebaseData);
}),
[]
);
itemsSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
items: [],
};
const itemsSlice = createSlice({
name: "addItems",
initialState,
reducers: {
addItem: (state, action) => {
state.items.push(...action.payload);
},
emptyItems: (state) => {
state.items = [];
},
},
});
export const { addItem, emptyItems } = itemsSlice.actions;
export default itemsSlice.reducer;
Here is a gif of the issue:
https://gyazo.com/bf033f2362d9d38e9eb315845971e224
Since I don't understand how you're switching between tabs, I can't tell you why the component seems to be rerendering. The useEffect hook is responsible to act like both the componentDidMount and componentDidUpdate methods. And the global redux state is not going to reset itself after each render (which is why it's so great).
This is not the case for a component's state which well reset each time a component is removed from and added to the dom. This explains why when you use a local state, the problem disappears. It's not that its fixed, it's because the state is reset every time the component is removed and added (which is what seems to be happening here).
Since we don't have the full details a quick fix should be to replace this
state.items.push(...action.payload);
with this
state.items = [...action.payload];
this will set a new value to state.items instead of pushing to it.
I'm new in Redux and have a problem with rerendering after the store changed. I have found many similar problems here on SO but still can't solve my issue.
I have a monthly task(event) calendar with multiple tasks. The Calendar is the main component and some level deeper there are multiple TaskItem components. At the first render, the calendar and the tasks are rendered fine (In this case without employee names). In the Calendar component I trigger loading employees with a useEffect hook. I can see the network request on my console. Besides this, the console logs in the action, and in the reducer also show the employee list. And the Redux devtool also shows the loaded employees. Still the mapStateToProps on TaskItem shows a completly empty state.
What I'm doing wrong?
Here is my related code:
Calendar:
const Calendar = ({startDay, tasks, loadEmployeesAction}) => {
useEffect(()=>{
loadEmployeesAction();
},[]);
...
}
export default connect(null, {loadEmployeesAction})(Calendar);
TaskItem:
const TaskItem = ({task, onTextEdit, onTaskView, saveTask, employees }) => {
...
}
const mapStateToProps = (state) => {
console.log('Actual state is: ', state);
return {
employees: state.employees
}
}
export default connect(mapStateToProps)(TaskItem);
Reducer:
export const employeeReducer = (state = [], action) => {
switch (action.type) {
case actionType.EMPLOYEES_LOADED:
console.log('Reducer - Employees loaded:', action );
return action.payload.employees;
default :
return state;
}
}
Actions:
const employeesLoaded = (employees) => {
return {type: actionType.EMPLOYEES_LOADED, payload: {
employees
}
}
}
export const loadEmployeesAction = () => {
return (dispatch) => {
return employeeApi.getAllEmployees().then(emps => {
console.log('Action - Employees loaded: ', emps);
dispatch(employeesLoaded(emps));
})
}
}
Root reducer:
export const rootReduxReducer = combineReducers({
employees: employeeReducer
});
I found the error. It was a very clumsy mistake.
All of my posted code was fine, but I put the store creation in a component that was rerendered again and again so my store was recreated again and again.
The reducer code seems to be not as the redux pattern. So usually the state object is not directly replaced with a different object. Instead only the part of the state that needs to be changed is only with some non-mutating operation like spread operator.
So I think the reducer code should be changed like
export const employeeReducer = (state = [], action) => {
switch (action.type) {
case actionType.EMPLOYEES_LOADED:
return {...state,employees:action.payload.employees}
default :
return state;
}
}
if the response from the API is in the form
[{"employee_name":"name","employee_age":24},.....]
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.
lately i'm facing a tough issue with React/Redux (Thunk): i've created my Store with Action and Reducer properly, in my Component i trigger the Async function in componentDidMount method in order to update the state, But the State doesn't seems to be changing, although it does changed in componentDidUpdate and mapStateToProps functions ! Why ? Here is my code :
export const getAllInterventions = () => {
return dispatch => {
dispatch(getAllDataStart());
axios.get('/interventions.json')
.then(res => {
dispatch(getAllDataSuccess(res.data));
})
.catch(err => {
dispatch(getAllDataFail(err));
});
};
My reducer :
case actionTypes.GET_ALL_INTERVENTIONS_SUCCESS:
return {
...state,
interventions: interventions: action.interventions
};
My Component:
componentDidMount() {
this.props.getAllInterventions();
console.log('DidMount: ', this.props.inter); /*Empty Array Or Undefined */
}
const mapStateToProps = state => {
console.log('mapStateToProps', state);
return {
inter: state.interventions,
error: state.error
};
Your action and state changes are both asynchronous, doing console.log right after will always return before those changes are actually completed.
Your state is being updated, which is why it works when you console.log in the mapStateToProps.
I suggest setting up redux-devtools, you'll be able to easily track your actions/state.
I hope the code can be usefully for you
componentWillReceiveProps(nextProps){
console.log(nextProps.inter)
}