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.
Related
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'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?
Not able to access the redux store current state in a Class component.
It shows up console error
Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it.
When I tried to implement the same using a function component with useSelector and useDispatch, everything works as expected. What has gone wrong over here?
reducer.js
let initialState={
count:0
}
const reducer=(state=initialState,action)=>{
switch(action.type){
case ADD_INCREMENT:
return {
...state,
count:state.count+1
};
default: return state;
}
}
export default reducer;
action.js
const Increment=()=>{
return {
type:ADD_INCREMENT
}
}
store.js
import reducer from './reducer';
const store=createStore(reducer);
export default store;
Class Component
import { connect } from 'react-redux';
const mapStateToProps=state=>{
return {
count:state.count
}
}
const mapDispatchToProps=(dispatch)=>{
return {
count:()=>dispatch(action.Increment())
}
}
class Orders extends Component {
render() {
return (
<div>
<h1>Count: {this.props.count} </h1>
</div>
);
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Orders);
In App.js the entire container is wrapped with Provider and store is passed as props
Issue
You've named your state and your action both count, the latter is the one injected as a prop.
const mapStateToProps = state => {
return {
count: state.count // <-- name conflict
}
}
const mapDispatchToProps = (dispatch) => {
return {
count: () => dispatch(action.Increment()) // <-- name conflict
}
}
Solution
Provide different names, count for the state, maybe increment for the action.
const mapStateToProps = state => ({
count: state.count,
});
const mapDispatchToProps = (dispatch) => ({
increment: () => dispatch(action.Increment())
})
I don't understand why React not update my object. In another component through the dispatch I update the state. In this (in code below) code in mapStateToProps categories are changing (console log show one more category). But component not rerender, although in component in useEffect I use props.categories. Event console.log in element does not run
const LeftSidebar = (props: any) => {
console.log('not render after props.categories changed')
useEffect(() => {
props.dispatch(getCategories())
}, [props.categories]);
const addCategoryHandler = (categoryId: number) => {
props.history.push('/category/create/' + categoryId)
};
return (
<div className='left-sidebar'>
<Logo/>
<MenuSidebar categories={props.categories} onClickAddCategory={addCategoryHandler}/>
</div>
);
};
function mapStateToProps(state: State) {
const categories = state.category && state.category.list;
console.log('this categories changes, but LeftSidebar not changing')
console.log(categories)
return { categories };
}
export default connect(mapStateToProps)(LeftSidebar);
I thought if i update state, react update components dependent on this state. How should it work? how should it work? It may be useful, the item that adds the category is not a parent or child, it is a neighbor
My reducer
import {CATEGORIES_GET, CATEGORY_CREATE} from "../actions/types";
export default function (state={}, action: any) {
switch (action.type) {
case CATEGORIES_GET:
return {...state, list: action.payload};
case CATEGORY_CREATE:
return {...state, list: action.payload};
default: return state;
}
}
Thanks for solving problem. All problem was in inmutable data. I used fixtures, and not copied properly array
import {CATEGORIES_GET, CATEGORY_CREATE} from "./types";
import {categoryMenuItems as items} from "../../fixtureData";
import {NewCategory} from "../../types";
let categoryMenuItems = items; // My mistake, I used not immutable value. Not use fixtures for state))
let id = 33;
export function getCategories() {
return {
type: CATEGORIES_GET,
payload: categoryMenuItems
}
}
export function createCategory(newCategory: NewCategory) {
id++
const category = {
title: newCategory.name,
id: id
};
// MISTAKE I use same array, not cloned like let clonedCategoryMenuItems = [...categoryMenuItems]
categoryMenuItems.push(category);
return {
type: CATEGORY_CREATE,
payload: categoryMenuItems
}
}
Not use fixtures for state, use real api :)
Maybe your state not is inmutable. In your reducer use spread operator to add new items
{
list: [
...state.list,
addedCategory
]
}
Instead of
state.list.push(addedCategory)
Is is correct to pass a reducer as props when i'm using a rootreducer ?
This is my rootReducer.js :
import { combineReducers } from 'redux';
import simpleReducer from './simpleReducer';
import messageReducer from './messageReducer';
import NewReducer from './NewReducer';
export default combineReducers({
simpleReducer,messageReducer,NewReducer
});
And this is one of my action creators addMessage.js
export const addMessage = (message) => dispatch => {
dispatch({
type: 'ADD',
message: message
})
}
Here is the first reducer messageReducer.js
export default (state = [], action) => {
switch (action.type) {
case 'ADD':
return [
...state,
action.message
];
default:
return state;
}
};
And here is another one simpleReducer.js
export default (state = {}, action) => {
switch (action.type) {
case 'SIMPLE_ACTION':
return {
result: action.payload
}
default:
return state
}
}
And finally here is my last reducer NewReducer.js
export default (state = '', action) => {
switch (action.type) {
case 'AnyThing':
return action.WhatToDisplay;
default:
return state;
}
};
Here is my mapping in the App.js
const mapStateToProps = state => ({
...state
})
const mapDispatchToProps = dispatch => ({
simpleAction: () => dispatch(simpleAction()),
submitNewMessage: (message) => {
dispatch(addMessage(message))
},
NewAction: () => dispatch(NewAction())
})
And here is my ِApp Component.Notice my last 2 h2 tags as well as my ul tag .Without me adding the reducer at the end of the prop , it doesn't work.So
is what i'm doing right ? or is there another way to show the redux state in
my react ?.Note that i currently have no errors and the code functions well.I
just wana know if what i am doing is right or wrong and if there is a better
syntax to show the redux state in my create react app.
class App extends Component {
constructor(props) {
super(props);
this.state = {
input: ''
}
this.handleChange = this.handleChange.bind(this);
this.submitMessage = this.submitMessage.bind(this);
}
handleChange(event) {
this.setState({
input: event.target.value
});
}
submitMessage() {
this.props.submitNewMessage(this.state.input);
this.setState({
input: ''
});
}
simpleAction = (event) => {
this.props.simpleAction();
}
localNormalFunction=(event)=>{
this.props.NewAction()
}
render() {
return (
<div >
<h1>fjasgdasdsg</h1>
<button onClick={this.simpleAction}>Test redux action</button>
<pre>
{
JSON.stringify(this.props)
}
</pre>
<h2>Type in a new Message:</h2>
<input
value={this.state.input}
onChange={this.handleChange}/><br/>
<button onClick={this.submitMessage}>Submit</button>
<ul>
{this.props.messageReducer.map( (message,idx) => {
return (
<li key={idx}>{message}</li>
)
})
}
</ul><br/><br/>
<button onClick={this.localNormalFunction}>dsadsdsa</button>
<h2>{this.props.NewReducer}</h2>
<h2>{this.props.simpleReducer.result}</h2>
</div>
);
}
}
It is better practice to get only the props you need from redux in each component. If you pass the whole redux state in mapStateToProps then whenever anything in redux changes you will have everything rerendering even if nothing you use changed.
One common reason you might be getting errors is that you are trying to use the props in render and they get instantiated afterwards.
Try this give default values to the props if you can't get them from redux:
App.defaultProps = {
result: '',
NewReducer: '',
messageReducer: []
}
const mapStateToProps = state => ({
result: state.simpleReducer.result,
NewReducer: state.NewReducer,
messageReducer: state.messageReducer
})
and then change this.props.simpleReducer.result to this.props.result