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?
Related
I have a basic job board application. An API is called within the redux store (using thunk function) and initial job results are then saved in redux store.
Ref: https://redux.js.org/tutorials/essentials/part-5-async-logic
These initial Jobs are stored in redux store (and not in local component state), as I need to access these initial job results in other components as well
There are also three filters that can be applied to these initial jobs (Jobs can be filtered by location, team and commitment) I've put these filters inside the redux store as well. (Actions are triggered from
Filter UI component to update the current applied filters, and multiple filters can be active at one time)
The Filter UI component pretty much just renders a <Select> element with a handleChange function which causes the filters to update in the redux store, something like this:
Basic Filter UI Component which dispatches action :
<Select
name={name}
value={value}
onChange={handleChange}
></Select>
// ... omit some code ...
const handleChange = (event) => {
const { name } = event.target;
switch (name) {
case 'location':
dispatch(changeLocationFilter(event.target))
break;
case 'team':
dispatch(changeTeamFilter(event.target))
break;
case 'commitment':
dispatch(changeCommitmentFilter(event.target))
break;
}
}
Here is my filtersSlice in redux, which update the redux state when filters are applied:
import { createSlice } from "#reduxjs/toolkit";
import { ALL_LOCATIONS, ALL_TEAMS, ALL_COMMITMENTS } from '../constants'
const initialState = {
location: ALL_LOCATIONS,
team: ALL_TEAMS,
commitment: ALL_COMMITMENTS
};
export const filtersSlice = createSlice({
name: "filters",
initialState,
reducers: {
changeLocationFilter: (state, action) => {
const { payload: { value: locationValue } } = action;
state.location = locationValue;
},
changeTeamFilter: (state, action) => {
const { payload: { value: teamValue } } = action;
state.team = teamValue;
},
changeCommitmentFilter: (state, action) => {
const { payload: { value: commitmentValue } } = action;
state.commitment = commitmentValue;
}
}
});
// Action creators are generated for each case reducer function
export const { changeLocationFilter, changeTeamFilter, changeCommitmentFilter } = filtersSlice.actions;
export default filtersSlice.reducer;
Every time those filters change, I'm using a memoized createSelector function to get those updated filters, then I'm filtering my jobs locally within my JobContainer component
Ref:
https://redux.js.org/tutorials/essentials/part-6-performance-normalization
Ref:
https://redux-toolkit.js.org/api/createSelector
I am not updating the jobs in the redux store (From initial jobs to filtered jobs) because after doing some reading, it seems that when it comes to filtering data, the generally accepted best practice is to do this via derived state, and there is no need to put this inside component state or redux store state -
Ref:
What is the best way to filter data in React?
Here is some code to illustrate my example further:
Here is my JobsContainer component, which get the initial jobs and the filters from the redux store, and then filters the jobs locally:
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { createSelector } from "reselect";
import Job from "../../components/Job";
import { ALL_LOCATIONS, ALL_TEAMS, ALL_COMMITMENTS } from '../../constants'
import { fetchReduxJobs, selectAllReduxJobs } from '../../redux/reduxJobs'
const JobsContainer = () => {
const dispatch = useDispatch()
const reduxJobsStatus = useSelector(state => state.reduxJobs.status);
let reduxJobs = useSelector(selectAllReduxJobs); // GET INITIAL JOBS FROM REDUX STATE HERE
const filterState = useSelector((state) => state.filters); // GET FILTERS FROM REDUX STATE HERE
const selectLocation = filterState => filterState.location
const selectTeam = filterState => filterState.team
const selectCommitment = filterState => filterState.commitment
// CREATE MEMOIZED FUNCTION USING CREATESELECTOR, AND RUN A FILTER ON THE JOBS
// WHENEVER FILTERS CHANGE IN REDUX STORE
const selectFilters = createSelector([selectLocation, selectTeam, selectCommitment], (location, team, commitment) => {
let tempReduxJobs = reduxJobs;
tempReduxJobs = tempReduxJobs.filter((filteredJob) => {
return (
(location === ALL_LOCATIONS ? filteredJob : filteredJob.categories.location === location) &&
(commitment === ALL_COMMITMENTS ? filteredJob : filteredJob.categories.commitment === commitment) &&
(team === ALL_TEAMS ? filteredJob : filteredJob.categories.team === team)
)
})
return tempReduxJobs;
})
reduxJobs = selectFilters(filterState); // UPDATE JOBS HERE WHEN FILTERS CHANGE
let content;
if (reduxJobsStatus === 'loading') {
content = "Loading..."
} else if (reduxJobsStatus === 'succeeded') {
// JUST MODIFYING MY JOBS A BIT HERE BEFORE RENDERING THEM
let groupedReduxJobs = reduxJobs.reduce(function (groupedObj, job) {
const { categories: { team } } = job;
if (!groupedObj[team]) {
groupedObj[team] = []
}
groupedObj[team].push(job)
return groupedObj
}, {})
// THIS IS HOW I RENDER MY JOBS HERE AFTER MODIFYING THEM
content = Object.keys(groupedReduxJobs).map((teamName, index) => (
<div key={index}>
<div className="job-team-heading">{teamName}</div>
{groupedReduxJobs[teamName].map((job) =>
(<Job jobDetails={job} key={job.id} />))
}
</div>
))
// return groupedObj
} else if (reduxJobsStatus === 'failed') {
content = <div>{error}</div>
}
useEffect(() => {
if (reduxJobsStatus === 'idle') {
dispatch(fetchReduxJobs())
}
}, [reduxJobsStatus, dispatch])
return (
<JobsContainerStyles>
<div>{content}</div>
</JobsContainerStyles>
);
}
export default JobsContainer;
Something about how Im updating my jobs after the filters change (inside JobsContainer) using my selectFilters function ie the line:
reduxJobs = selectFilters(filterState);
Seems off. (Note: as you can see, I am modifying the data a bit before rendering as well - see groupedReduxJobs)
I wouldn't be as confused if I was to update the redux store with the filtered jobs after the filter is applied, but as I mentioned, reading into this topic suggests filtered data should generally be kept as derived state, and not in redux store. This is what I am confused about.
Can someone provide some constructive criticism on how I'm doing this please ? Or is the way Im doing this currently a good way to go about solving this problem.
To clarify, this is all working as written here .. but I'm not sure what other's opinions are on doing it this way vs some other way
I have an input field where I am trying to pass some information before moving onto a separate page. My problem is the Redux state is not changing, but the console is showing the value is being passed correctly. I'm assuming something is wrong with my Slice but I believe I am passing the payload correctly. My Redux slice looks like:
import { createSlice } from "#reduxjs/toolkit";
export const walletSlice = createSlice({
name: "wallet",
initialState: {
wallet: "xxx-xxxx-xxx-xxxx",
},
reducers: {
setWalletAddress: (state, action) => {
state.value = action.payload;
},
},
});
export const { setWalletAddress } = walletSlice.actions;
export default walletSlice.reducer;
While my from component looks like:
import { setWalletAddress } from "../../redux/wallet";
import { useDispatch } from "react-redux";
export default function AddressForm() {
return (
const dispatch = useDispatch();
const handleChangeWallet = (event) => {
dispatch(setWalletAddress (event.target.value));
console.log(event.target.value);
};
<React.Fragment>
<TextField
onChange={handleChangeWallet}
label="Wallet address"
/>
</React.Fragment>
);
}
My store looks pretty standard:
export default configureStore({
reducer: {
wallet: walletReducer,
},
});
I assume that you need to use correct state field name in the reducer function. I guess following code line makes an issue,
state.value = action.payload;
Instead of this, you need to write correct field name wallet not value
state.wallet = action.payload;
You've mistyped value instead of wallet
setWalletAddress: (state, action) => {
state.wallet = action.payload;
},
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.
I've tried to search about for this issue but most of the topic is about mutate but it's a primitive datatype in my case and here is the example code:
// ParentPage.jsx
import { useSelector } from 'react-redux';
const selectShowFooterFlag = (state) => {
return state.shouldShowFooter;
}
const ParentPage = () => {
const shouldShowFooter = useSelector(selectShowFooterFlag);
const ChildComponent = useSelector(selectChildComponent);
return (
<p>{String(shoudShowFooter)}</p>
<ChildComponent shoudShowFooter={shoudShowFooter}>
)
}
export default ParentPage;
// ChildPage
const ChildPage = ({ shoudShowFooter }) => {
useEffect(() => {
dispatch(shouldShowFooterState(false));
return () => {
dispatch(shouldShowFooterState(true));
}
});
return (
<p>String(shoudShowFooter)</p>
)
}
// reducer
const initalState = {
isPageFooterRequired: true,
};
export const pagesReducer: (draft, action) = produce((draft = initialState, action: DispatchAction) => {
switch() {
case SHOW_FOOTER:
draft.shoudShowFooter = action.payload;
break;
default:
return draft;
}
});
With this example, when child component mounted, it will set shoudShowFooter to false as expected. When ChildComponent updated in redux store, the current ChildComponent will unmount and set the shoudShowFooter to true. I've checked that and it's correct as expected (the selectShowFooterFlag has been called).
But the issue is that shoudShowFooter changed in redux but ParentPage not re-render with new state so that new ChildComponent can't receive the latest data in reducer store (I've checked with the latest state data by Redux devtools).
One more interesting point is that when I switch to use connect, everything working just fine.
import { connect } from 'react-redux';
const selectShowFooterFlag = (state) => {
return state.shouldShowFooter;
}
const ParentPage = () => {
const ChildComponent = useSelector(selectChildComponent);
return (
<p>{String(shoudShowFooter)}</p>
<ChildComponent shoudShowFooter={shoudShowFooter}>
)
}
const mapStateToProps = (state) => {
return {
shouldShowFooter: selectShowFooterFlag(state);
}
}
export default connect(mapStateToProps)(ParentPage);
I think useSelector will give us the same behavior like connect, will re-render component when state data change. But seems like it's not in this case.
How could I make useSelector trigger re-render with latest data like what connect does? Or how could I debug this case to see why React not re-render the app?
new user to Redux so apologies for any silly mistakes. Ultimately, I am trying to toggle a className in component B as an onClick function toggles a state in component A.
So it should be:
Component A button click => state toggles => Component B className toggles.
I have setup my store and my mapDispatchToProps function that changes the state in my store seems to be working. However, calling this state in a different component is working... but, does not re-render as the state in the store toggles. There is more content in my code, but I've tried to strip the parts that are not necessary to this issue.
Component A - containing a button that toggles a state and changes a state within my store/rootReducer:
import React from 'react';
import { connect } from 'react-redux';
function Nav(props) {
const [showMenu, setShowMenu] = React.useState('menuClosed');
function menuClick() {
showMenu === 'menuClosed' ? setShowMenu('menuOpen') :
setShowMenu('menuClosed');
}
// this works fine, I know this due to the console.log on my rootReducer page:
React.useEffect(() => {
props.toggleMenu(showMenu);
}, [showMenu])
return (
<button className="hvr-icon-grow-rotate" id="bars" onClick={() => { menuClick(); }}>
<FontAwesomeIcon icon={faBars} className="hvr-icon" />
</button>
)
}
const mapStateToProps = (state) => {
return {
menu: state.menu
}
}
const mapDispatchToProps = (dispatch) => {
return {
toggleMenu: (showMenu) => {
dispatch({ type: 'TOGGLE_MENU', menu: showMenu })
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Nav)
Component B: supposed to toggle a className depending on store state:
import React from 'react';
import { connect } from 'react-redux';
const [noHover, setNoHover] = React.useState('projectGrid');
React.useEffect(() => {
console.log('portfolio props: ' + props.menu + ' noHover: ' + noHover);
if (props.menu === 'menuOpen') {
setNoHover('projectGrid noHover');
console.log('portfolio props: ' + props.menu + ' noHover: ' + noHover);
}
else if (props.menu === 'menuClosed') {
setNoHover('projectGrid');
console.log('portfolio props: ' + props.menu + ' noHover: ' + noHover);
}
}, [])
return (<div className={noHover}>some content</div>);
}
const mapStateToProps = (state) => {
return {
menu: state.menu
}
}
export default connect(mapStateToProps)(PortfolioItem)
finally, content of my rootReducer.js page, or the Redux store:
const initState = {
menu: ['menuClosed']
}
const rootReducer = (state = initState, action) => {
console.log(action);
return state;
}
export default rootReducer;
Any help would be greatly appreciated - thank you!
It doesn't look like you're actually toggling anything in your reducer.
You need to return a new state if you want it to change. I'm not sure exactly how it should look in your case, but something like this:
const rootReducer = (state = initState, action) => {
let newState = {...state}; // Create a new state object so we don't mutate original
switch(action.type) { // Check what type was sent to see if we should update
case 'TOGGLE_MENU':
newState.menu = action.menu; // Set new state based on action
return newState;
default:
return state; // Return old state if nothing changed
}
}
Side note: it looks like your initial state has menu as an array, but your components don't treat it as one. It seems like it should be defaulted to a string.
It looks like #brian-thompson has solved your problem, so I won't solve it again, but I would say you can make your styling logic a log easier by using the classnames library.
Your useEffect callback would be replaced with
const { menu } = props
const noHover = classNames({
'projectGrid': true,
'noHover': menu === 'menuOpen'
})
Removing the need for any hooks.