I have this redux todo app that updates the state of the remaining tasks based on the number of incomplete tasks.
The app is working without any errors or problems but when I add a task, toggle completion, and remove a task, the action type of remainingTasks/updateRemainingTasks fires twice:
Interestingly, that action only fires once when removing a task that has been completed:
These are the code for that slice and its corresponding component:
SLICE
import { createSlice } from "#reduxjs/toolkit";
const remainingTasksSlice = createSlice({
name: "remainingTasks",
initialState: 0,
reducers: {
updateRemainingTasks: (state, action) => {
return action.payload;
},
},
});
// Selectors
export const selectRemainingTasksSlice = (state) => state.remainingTasksReducer;
// Actions
export const { updateRemainingTasks } = remainingTasksSlice.actions;
// Reducers
export default remainingTasksSlice.reducer;
COMPONENT
import { useSelector, useDispatch } from "react-redux";
import {
selectRemainingTasksSlice,
updateRemainingTasks,
} from "./remainingTasksSlice";
import { selectTaskSlice } from "../task/taskSlice";
const RemainingTasks = () => {
const dispatch = useDispatch();
const remainingTasksSlice = useSelector(selectRemainingTasksSlice);
const taskSlice = useSelector(selectTaskSlice);
// Number of Incomplete Tasks
const incompleteTasks = taskSlice.filter((task) => !task.completed).length;
// Update the State of the Remaining Tasks
dispatch(updateRemainingTasks(incompleteTasks));
return (
<div>
<h1 className="header">
{remainingTasksSlice > 1
? `${remainingTasksSlice} Tasks Left`
: `${remainingTasksSlice} Task Left`}
</h1>
</div>
);
};
export default RemainingTasks;
I was wondering if this is a normal thing or my code isn't well optimized.
I think you have to call dispatch into a useEffect hook:
....
useEffect(()=>{
// Number of Incomplete Tasks
const incompleteTasks = taskSlice.filter((task) => !task.completed).length;
// Update the State of the Remaining Tasks
dispatch(updateRemainingTasks(incompleteTasks));
}, [taskSlice]);
....
otherwise you call dispatch every time you render the Component.
Related
This the code that is responsible for getting Data and updating the list:
import {createSlice} from "#reduxjs/toolkit";
import axios from "axios";
const initialState = {
log: []
}
let page = 1;
const cartSlice = createSlice({
name: 'cart',
initialState: initialState,
reducers: {
fetching: (state, payload) => {
state.log = axios.get(`http://localhost:5000/fetch/?${page = payload.payload}`)
// console.log(state)
}
}
})
export const {fetching} = cartSlice.actions;
export default cartSlice.reducer;
And this is the Home Page:
import React, {useEffect} from "react";
import {useDispatch, useSelector} from "react-redux";
import {fetching} from "../features/cardSlice";
import Cards from "./Cards";
export default function HomePage(){
const dispatch = useDispatch();
const {itemsList} = useSelector((store) => store.card)
console.log(itemsList)
const {pageNumber} = useSelector((store) => store.page);
useEffect(()=>{
dispatch(fetching(1)); **// This is where i call dispatch to update the state and get the data**
})
function cardMapper(items) {
return(
<Cards
name = {items.name}
key = {items.id}
cuisine={items.cuisine}
address={items.address}
/>
)
}
return(
<div>
{/*{itemsList.map(cardMapper)}*/}
</div>
)
}
When i run this on localhost i am not able to get data, the console.log(itemList) is showing undefined and also the dispatch(fetching(1)) is called infinite times.
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
I am not able to understand why i'm getting an infinite loop and also why am i not getting the data.
I modified your code. To prevent infinite loop in using useEffect you should put [] as dependency
And for the useSelector,the variable should not enclosed by {}.
see the modified code below.
As I see, it should be cart not card
Try this code instead
export default function HomePage(){
const dispatch = useDispatch();
const itemsList = useSelector((store) => store.cart)
console.log(itemsList)
const {pageNumber} = useSelector((store) => store.page);
useEffect(()=>{
dispatch(fetching(1)); **// This is where i call dispatch to update the state and get the data**
},[])
function cardMapper(items) {
return(
<Cards
name = {items.name}
key = {items.id}
cuisine={items.cuisine}
address={items.address}
/>
)
}
return(
<div>
{/*{itemsList.map(cardMapper)}*/}
</div>
)
}
also in reducer
add a return
fetching: async (state, payload) => {
const newState = await axios.get(`http://localhost:5000/fetch/?${page = payload.payload}`)
return newState;
}
// this should end the infinite loop
useEffect(()=>{
dispatch(fetching(1)); // This is where i call dispatch to update the state and get the data
}, [ ])
You need added dependencies as an empty array. To remove the yellow
squiggly line you need to add dispatch inside that dependencies array.
however, react to ensure that dispatch dependencies never change and
that you are not responsible for re-render your component.
useEffect(()=>{
dispatch(fetching(1))
},[dispatch])
Basically I got a state called optimizer in which I store a field named optimizer_course_entries , this field has 2 reducers on it:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
optimizer_course_entries : [],
}
export const optimizerSlice = createSlice(
{
name: 'optimizer',
initialState,
reducers: {
edit_entry: (state, action) => {
console.log('CALLED EDIT REDUCERS');
state.optimizer_course_entries[action.payload.index] = action.payload.value;
},
reset: (state) => {
console.log('CALLED RESET REDUCER');
state.optimizer_course_entries = [];
}
}
}
)
export const {edit_entry, reset} = optimizerSlice.actions;
export default optimizerSlice.reducer;
In my react app, I have a call to edit_entry everything a textbox is edited, and it sends the index and value in a payload to Redux.
const receiveChange = (Value, Index) => {
dispatch(edit_entry({
index : Index,
value : Value,
}));
}
I have the reset reducer set on component mount like this:
React.useEffect(
() => {
dispatch(reset());
} , []
)
The issue i'm having is that on component mount, instead of redux only doing a reset, it also restores previous reducer actions..
And in my redux store, the optimizer_course_entries entry is identical to before the reset...
I'm still pretty new to redux, is there a way I can specify it so that upon re-mount it doesn't do this repopulation?
I have looked into multiple sources trying to solve this problem but could not find any answers. I have a functional component <Dashboard /> which will display some information from an API.
I expected the component to first get into useEffect, execute the getData function and then display {devices} on the screen. What happens, though, is that the store state is updated, but the component not. The {devices} variable is always undefined. I don't think I understand how to access my state variable from reducers/all/dashboard.js with useSelector.
dashboard/index.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import api from "../../services/api";
import * as DashboardActions from "../../store/actions/dashboard";
const Dashboard = (props) => {
const dispatch = useDispatch();
const devices = useSelector(state => state.device)
useEffect(() => {
async function getData() {
const pathname = "/dashboard";
await api
.get(pathname)
.then((res) => {
dispatch(DashboardActions.setData(res.data));
})
.catch((res) => {
console.log(res.response.data);
});
}
getData();
console.log("devices ue ", devices);
}, [dispatch]);
return (
<div>
<h1>Dashboard</h1>
<span>{devices}</span>
</div>
);
};
export default Dashboard;
reducers/all/dashboard.js
const INITIAL_STATE = {
devices: [],
};
function dashboard(state = INITIAL_STATE, action) {
console.log("Action ", action)
if ("DASHBOARD_SET_DATA" === action.type) {
const data = action.data;
console.log("Data: ", data.devices)
state = { ...state, devices: data.devices };
console.log("State ", state)
}
return state;
}
export default dashboard;
actions/dashboard.js
export function setData(data) {
return {
type: "DASHBOARD_SET_DATA",
data,
};
}
I would appreciate any help a lot.
Thanks in advance!
The react-redux useSelector hook is selecting state from your redux store state object.
If your dashboard reducer is combined into your root reducer, something like
const rootReducer = combineReducers({
... other reducers
dashboard,
... other reducers
});
Then the devices state value should be accessed from state.dashboard.devices.
The update for your component:
const devices = useSelector(state => state.dashboard.devices)
I'm trying to setup up my react project with redux and I'm following a basic example with a counter, which I can increment and decrement. The counter displays correctly on the page as 0 initially - however when I hit the button, the increment action doesn't seem to be dispatched, and as a consequence, the counter does not update.
My LoginPage.js:
/* eslint-disable no-unused-expressions */
import { connect } from "react-redux";
import React, { Component } from "react";
import { selectCounter } from "./../../selectors/counter";
import { actions as counterActions } from "./../../actions/counter";
class LoginPage extends Component {
componentDidMount() {}
render() {
const { counter, increment } = this.props;
return (
<div>
<p>{`Hi ${counter}`}</p>
<button onClick={() => increment()}>+</button>
</div>
);
}
}
LoginPage = connect(
(state, props) => ({
counter: selectCounter(state, props)
}),
{ ...counterActions }
)(LoginPage);
export default LoginPage;
My actions/counter.js:
import { INCREMENT } from "./../types/counter";
const increment = () => {
return { type: INCREMENT };
};
export const actions = {
increment
};
My /reducers/counter.js:
const { INCREMENT, DECREMENT } = "./../types/counter";
const counterReducer = (state = 0, action) => {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
};
module.exports = { counterReducer };
My /reducers/index.js:
import { combineReducers } from "redux";
import { counterReducer } from "./counter";
const rootReducer = combineReducers({
counter: counterReducer
});
export default rootReducer;
I'm omitting the App.js and index.js files as these are pretty simple and don't seem to be related to the problem.
UPDATE:
My actions/counter.js:
import { INCREMENT } from "./../types/counter";
import { useDispatch } from "react-redux";
const increment = () => {
return { type: INCREMENT };
};
const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch(increment())
};
};
export const actions = {
...mapDispatchToProps(useDispatch)
};
Now I am seeing the bug:
react-dom.development.js:14724 Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
Updated
Need to properly define mapDispatchToProps function and pass it to connect(). In your code increment() doesn't seem to dispatch an action.
const mapDispatchToProps = (dispatch) =>{
increment: ()=>dispatch(actions.increment())
}
LoginPage = connect(
(state, props) => ({
counter: selectCounter(state, props)
}),
mapDispatchToProps
)(LoginPage);
Update
The error is due to useDispatch() usage outside component. It has to be declared and used within a functional component.
I have a functional component that fetches data from an api using redux.
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
The component:
export function Trips(props) {
const trips = useSelector(state => state.trips);
useFetching(fetchTrips)
...
...
}
The thunk:
export const fetchTrips = () => (dispatch) =>
axios.get("/api/v1/trips")
.then(response => dispatch(addTrips(response.data)))
export const addTrips = trips => ({
type: ADD_TRIPS,
payload: trips
})
The reducer:
function tripsReducer(state = INITIAL_STATE, action) {
console.log(action)
if (action.type === ADD_TRIPS) {
return Object.assign({}, state, {
trips: state.trips.concat(action.payload)
});
}
return state
}
My reducer is called. How can I update the UI after the fetched data have been dispatched? My render is not called again.
1st option: Using hooks
You are actually using React and react-redux hooks. Make sure you use the object trips later in your component. Here is a sample using your code:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchTrips } from '../tripsActions';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, []);
}
export function Trips(props) {
const trips = useSelector(state => state.trips);
useFetching(fetchTrips);
return (
<div>
<p>Total trips: {trips.length}</p>
</div>
);
}
2nd option: Using connect
This was the way to connect to the Redux store state before they introduced the hooks.
As you are using react-redux this can be easily done by using the connect() function. You should also provide a mapStateToProps() function to select the part of the data from the store that your component needs and a mapDispatchToProps() function to have access to the actions to be dispatched.
This is how your Trips component would look like with this approach:
import React from 'react';
import { connect } from 'react-redux';
import { fetchTrips } from '../tripsActions';
const mapStateToProps = (state) => {
return {
// will be available as props.trips
trips: state.trips
}
}
const mapDispatchToProps = (dispatch) => {
return {
// will be available as props.fetch()
fetch: () => dispatch(fetchTrips)
}
}
const function Trips(props) {
this.props.fetch();
// some other code. Example:
return (
<div>
<p>Total trips: {this.props.trips.length}</p>
</div>
);
}
export default connect(mapStateToProps)(Trips);
mapStateToProps() receives the Redux store state and returns an object whose fields will be available as props in your component. As you already use props.trips I simply mapped that field to the updated value of the Redux state's trips field.
The call to connect() with your component gives you a connected component. And that latter should be exported rather than the original component. It will not create another component so you will continue to use the Trips component normally.
Now your component will be re-rendered as its props are being updated.
You can have a look at the react-redux documentation to better understand the use of connect() and mapStateToProps() and mapDispatchToProps() functions.
You can do it easily like that:
import React, {
useCallback, useEffect
} from 'react';
import {
useSelector, useDispatch
} from 'react-redux';
// ../ducks for example
import { fetchTrips } from '../ducks';
const function Trips(props) {
const dispatch = useDispatch();
useEffect(() => {
fetchTripsHandler();
}, []);
const fetchTripsHandler = useCallback(
() => dispatch(fetchTrips()),
[]
);
const { trips } = useSelector(state => ({
trips: state.trips
}));
// your other component code
}
With useSelector from react-redux you can not use connect, mapStateToProps and mapDispatchToProps.
After that, your component will be connected to the Store.
Here is more information about react-redux hooks.