I am new to Redux and i've been having a hard time rendering changes made to the store. I've been using Redux-DevTools to explore state changes and here is my problem.
I have a sidebar which has expanded state as true/false. Initial state is below.
{expanded: false}
During the toggle action i trigger store.dispatch to change the state to true.
In React-DevTools i could see that my state is being changed, the console also logs the change when executed from within my sidenav component.
I have a home page which is able to fetch the initial state of the store, however the actions from sidenav which updates the store doesn't re-render(props val dint change) the home page.
I strongly feel the issue is related to this SO post but not able to get around the wrapped component concept. Any advice.
React Redux - changes aren't reflected in component
Below is code.
Redux Store
const rootReducer = combineReducers({
sideBarReducerMain: sidebarReducer })
export const configurestore = () => {
const store = createStore(rootReducer, composeWithDevTools(
));
return store; }
Action generators
export const onExpand = {
type: 'EXPAND',
payload: {
expanded: true
}
}
export const onCollapse = {
type: 'COLLAPSE',
payload: {
expanded: false
}
}
Reducer
export const sidebarReducer = (state = {expanded:false},action) => {
switch (action.type) {
case 'EXPAND':
return Object.assign({}, state, {expanded: true})
case 'COLLAPSE':
return Object.assign({}, state, {expanded: false})
default:
return state;
}
}
SideBar Toggle (The console logs the new state on every toggle)
onToggle = (expanded) => {
expanded ? store.dispatch(onExpand):store.dispatch(onCollapse)
this.setState({ expanded: expanded });
//outputs the state change looks fine.
console.log("State of store :: " + store.getState());
};
Home page
import React from 'react';
import styled from 'styled-components'
import Main from './MainAlign'
import { connect } from 'react-redux'
class HomePage extends React.Component {
getExpansionState() {
console.log("From render" + this.props.expandedState)
return this.props.expandedState
}
componentWillReceiveProps(nextProps) {
console.log("Looks like this never gets called :( " + this.props.expandedState)
}
render(props) {
return (
<div>
<Main expanded={this.getExpansionState()}>
<h1>Welcome</h1>
<p>This is my site. Take a look around!</p>
</Main>
</div>
)
}
}
const mapStateToProps = state => {
console.log("New state " + state.expanded)
return {
expandedState: state.expanded
}
}
export default connect(mapStateToProps)(HomePage);
REDUX DevTools
You need to declare action creators (functions) which return actions (objects):
export const onExpand = () => ({
type: "EXPAND",
payload: {
expanded: true
}
});
Then instead of calling store.dispatch, you should connect the action creator:
withRouter(connect(null, { onExpand, onCollapse })(SideBar))
And invoke it in your component:
if (expanded) {
this.props.onExpand();
} else {
this.props.onCollapse();
}
Working code:
Misc
this.setState({ expanded: expanded });
console.log("FROM SIDENAV" + this.state.expanded);
setState is asynchronous so you need use the callback to access the updated value:
this.setState({ expanded }, () => console.log("FROM SIDENAV" + this.state.expanded));
Also, there's no need to replicate the redux state in local component (single source of truth).
You can also combine onExpand and onCollapse into a single onToggle function and perform the toggle in reducer: expanded: !state.expanded, or pass the expanded flag as payload.
Change your mapStateToProps function like that:
const mapStateToProps = state => {
console.log("New state " + state.expanded)
return {
expandedState: state.sideBarReducerMain.expanded
}
}
You have a sideBarReducerMain state in your root,global state. This sideBarRecuerMain state has an expanded value not the global one.
Also, you don't need a payload for your action creators since you don't use them. Just a type is enough for your logic.
Related
I'm coming from redux + redux-saga and class component, everything went well when using componentDidMount in class component. Dispatching action to fetch api works well without duplicate request.
I've been learning functional component for a while, and decided to use Zustand to replace my redux-saga to handle my state management process. I've able to set state A from state B in reducer just by calling the action creators and the state get updated.
First of all, here's my react functional component code so far:
HomeContainer
import { useEffect } from "react";
import { appStore } from "../App/store";
export default function HomeContainer(props: any): any {
const getCarouselData = appStore((state: any) => state.getCarousels);
const carousels = appStore((state: any) => state.carousels);
useEffect(() => {
if (carousels.length === 0) {
getCarouselData();
}
}, [carousels, getCarouselData]);
console.log("carousels", carousels);
return <p>Home Container</p>;
}
Loading Slice
const loadingSlice = (set: any, get: any) => ({
loading: false,
setLoading: (isLoading: boolean) => {
set((state: any) => ({ ...state, loading: isLoading }));
},
});
export default loadingSlice;
App Store
import create from "zustand";
import homeSlice from "../Home/store";
import loadingSlice from "../Layout/state";
export const appStore = create((set: any, get: any) => ({
...loadingSlice(set, get),
...homeSlice(set, get),
}));
Coming to Zustand, it seems like the behaviour is different than Redux. I'm trying to update the boolean value of loading indicator with this code below:
import create, { useStore } from "zustand";
import axios from "axios";
import { appStore } from "../App/store";
const homeSlice = (set: any, get: any) => ({
carousels: [],
getCarousels: () => {
appStore.getState().setLoading(true);
axios
.get("api-endpoint")
.then((res) => {
set((state: any) => ({
...state,
carousels: res.data,
}));
})
.catch((err) => {
console.log(err);
});
appStore.getState().setLoading(true);
},
});
export default homeSlice;
The state is changing, the dialog is showing, but the component keeps re-render until maximum update depth exceeded. I have no idea why is this happening. How can I update state from method inside a state without re-rendering the component?
Any help will be much appreciated.
Thank you.
Update
The new instance of getCarousels is not created because of the dispatch since the create callback is called only once to set the initial state, then the updates are made on this state.
Original answer
Your global reducer is calling homeSlice(set, get) on each dispatch (through set). This call creates a new instance of getCarousels which is passed as a dependency in your useEffect array which causes an infinite re-rendering.
Your HomeContainer will call the initial getCarousels, which calls setLoading which will trigger the state update (through set) with a new getCarousels. The state being updated will cause the appStore hook to re-render the HomeContainer component with a new instance of getCarousels triggering the effect again, in an infinite loop.
This can be solved by removing the getCarouselsData from the useEffect dependency array or using a ref to store it (like in the example from the Zustand readme) this way :
const carousels = appStore((state: any) => state.carousels);
const getCarouselsRef = useRef(appStore.getState().getCarousels)
useEffect(() => appStore.subscribe(
state => (getCarouselsRef.current = state.getCarousels)
), [])
useEffect(() => {
if (carousels.length === 0) {
getCarouselsRef.current();
}
}, [carousels]); // adding getCarouselsRef here has no effect
I have a redux-toolkit store at store.js.
import { configureStore } from '#reduxjs/toolkit';
import productCurrency from './stateSlices/productCurrency';
const Store = configureStore({
reducer: {
productCurrency: productCurrency,
},
})
export default Store;
The createslice() function itself is in a different file and looks like this below.
import { createSlice } from '#reduxjs/toolkit'
const initialState = {
value: '$',
}
export const productCurrency = createSlice({
name: 'productCurrency',
initialState,
reducers: {
setproductCurrency(state, newState) {
state.value = newState
},
},
})
export const { setproductCurrency } = productCurrency.actions;
export default productCurrency.reducer;
My issue is that I have a class component NavSection that needs to access the initial state and the reducer action setproductCurrency() to change the state. I am trying to use the react-redux connect() function to accomplish that.
const mapStateToProps = (state) => {
const { productCurrency } = state
return { productCurrency: productCurrency.value }
}
const mapDispatchToProps = (dispatch) => {
return {
setproductCurrency: () => dispatch(setproductCurrency()),
dispatch,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(NavSection);
Now, I am able to access the state by ussing this.props.productCurrency. Yet, if I try to access the setproductCurrency() by ussing this.props.setproductCurrency()... Chrome console gives me an error that "this.props.setproductCurrency() is not a function".
Is there a way of fixing this, or am I trying to do something impossible?
UPDATE #1
I think I just moved the ball in the right direction. I changed the onClick function to be an arrow function as shown below.
onClick={() => this.props.setproductCurrency('A$')}
Now, setproductCurrency() is considered a function, but it returns a different error when I click the button...
Objects are not valid as a React child (found: object with keys {type, payload}). If you meant to render a collection of children, use an array instead.
Why would this function now return an object? It is supposed to change the state and trigger a re-render of the page so that the class component can access the newly changed state.
To be clear, RTK has nothing to do with React-Redux, connect, or mapDispatch :)
The current error of "Objects are not valid as a React child" is because your reducer is wrong. A reducer's signature is not (state, newState). It's (state, action). So, your line state.value = newStateis reallystate.value = action`, and that's assigning the entire Redux action object as a value into the state. That's definitely not correct conceptually.
Instead, you need state.value = action.payload.
I'm creating a chatroom app, and I've been learning Redux Toolkit as I do it. I currently have text input that will go to the messages data object when 'Send' is clicked; however, I can't seem to get the page to update and render new messages. Anyone know why?
Reducer:
import { createSlice, current } from '#reduxjs/toolkit'
import chatData from '../../components/chatData'
export const chatSlice = createSlice({
name: 'chat',
initialState: chatData[0],
reducers: {
addChat: (state, action) => {
if (action.payload !== '') {
state.messages.push({
avatar: "#",
username: "",
message: action.payload
})
console.log('after', current(state))
}
console.log(action.payload);
},
},
});
export const { addChat } = chatSlice.actions
export const selectChat = state => state.messages
export default chatSlice.reducer
JSX:
<div>
{props.chatData.messages.map((val, i) => {
return <MessageBox username={val.username} avatar={val.avatar} message={val.message} />
}
</div>
The state does update according to Redux DevTools and some console logging, but again, the page will not update accordingly.
If the state did update in the store then I believe the issue is caused by your component:
You did not map the state to your component correctly, maybe typos?
The component’s shouldComponentUpdate method was modified to prevent rerendering of the component.
Say I have two redux connected components. The first is a simple todo loading/display container, with the following functions passed to connect(); mapStateToProps reads the todos from the redux state, and mapDispatchToProps is used to request the state to be provided the latest list of todos from the server:
TodoWidgetContainer.js
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
The second redux component is intended to be applied to any component on a page so that component can indicate whether a global "loading" icon is displayed. Since this can be used anywhere, I created a helper function that wraps MapDispatchToProps in a closure and generates an ID for each component, which is used to make sure all components that requested the loader indicate that they don't need it anymore, and the global loader can be hidden.
The functions are basically as follows, with mapStateToProps exposing the loader visibility to the components, and mapDispatchToProps allowing them to request the loader to show or hide.
Loadify.js
function mapStateToProps(state) {
return {
openLoader: loaderSelectors.getLoaderState(state)
};
}
function mapDispatchToProps() {
const uniqId = v4();
return function(dispatch) {
return {
showLoader: () => {
dispatch(loaderActions.showLoader(uniqId));
},
hideLoader: () => {
dispatch(loaderActions.hideLoader(uniqId));
}
};
};
}
export default function Loadify(component) {
return connect(mapStateToProps, mapDispatchToProps())(component);
}
So now, if I have a component that I want to give access to the loader, I can just do something like this:
import Loadify from '...'
class DisplayComponent = new React.Component { ... }
export default Loadify(DisplayComponent);
And it should give it a unique ID, allow it to request the loader to show/hide, and as long as there is one component that is requesting it to show, the loader icon will show. So far, this all appears to be working fine.
My question is, if I would like to apply this to the todos component, so that that component can request/receive its todos while also being allowed to request the loader to show while it is processing, could I just do something like:
TodoWidgetContainer.js
import Loadify from '...'
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
const TodoContainer = connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
export default Loadify(TodoContainer);
And will redux automatically merge the objects together to make them compatible, assuming there are no duplicate keys? Or will it take only the most recent set of mapStateToProps/mapDispatchTo unless I do some sort of manual merging? Or is there a better way to get this kind of re-usability that I'm not seeing? I'd really rather avoid having to create a custom set of containers for every component we need.
connect will automatically merge together the combination of "props passed to the wrapper component", "props from this component's mapState", and "props from this component's mapDispatch". The default implementation of that logic is simply:
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
So, if you stack multiple levels of connect around each other , the wrapped component will receive all of those props as long as they don't have the same name. If any of those props do have the same name, then only one of them would show up, based on this logic.
Alright, here is what I would do. Create a higher order component (HOC) that adds a new spinner reference to your reducer. The HOC will initialize and destroy references to the spinner in redux by tying into the life cycle methods. The HOC will provide two properties to the base component. The first is isLoading which is a function that takes a boolean parameter; true is on, false is off. The second property is spinnerState that is a readonly boolean of the current state of the spinner.
I created this example without the action creators or reducers, let me know if you need an example of them.
loadify.jsx
/*---------- Vendor Imports ----------*/
import React from 'react';
import { connect } from 'react-redux';
import v4 from 'uuid/v4';
/*---------- Action Creators ----------*/
import {
initNewSpinner,
unloadSpinner,
toggleSpinnerState,
} from '#/wherever/your/actions/are'
const loadify = (Component) => {
class Loadify extends React.Component {
constructor(props) {
super(props);
this.uniqueId = v4();
props.initNewSpinner(this.uniqueId);;
this.isLoading = this.isLoading.bind(this);
}
componentWillMount() {
this.props.unloadSpinner(this.uniqueId);
}
// true is loading, false is not loading
isLoading(isOnBoolean) {
this.props.toggleSpinner(this.uniqueId, isOnBoolean);
}
render() {
// spinners is an object with the uuid as it's key
// the value to the key is weather or not the spinner is on.
const { spinners } = this.props;
const spinnerState = spinners[this.uniqueId];
return (
<Component isLoading={this.isLoading} spinnerState={spinnerState} />
);
}
}
const mapStateTopProps = state => ({
spinners: state.ui.spinners,
});
const mapDispatchToProps = dispatch => ({
initNewSpinner: uuid => dispatch(initNewSpinner(uuid)),
unloadSpinner: uuid => dispatch(unloadSpinner(uuid)),
toggleSpinner: (uuid, isOn) => dispatch(toggleSpinnerState(uuid, isOn))
})
return connect(mapStateTopProps, mapDispatchToProps)(Loadify);
};
export default loadify;
Use Case Example
import loadify from '#/location/loadify';
import Spinner from '#/location/SpinnerComponent';
class Todo extends Component {
componentWillMount() {
this.props.isLoading(true);
asyncCall.then(response => {
// process response
this.props.isLoading(false);
})
}
render() {
const { spinnerState } = this.props;
return (
<div>
<h1>Spinner Testing Component</h1>
{ spinnerState && <Spinner /> }
</div>
);
}
}
// Use whatever state you need
const mapStateToProps = state => ({
whatever: state.whatever.youneed,
});
// use whatever dispatch you need
const mapDispatchToProps = dispatch => ({
doAthing: () => dispatch(doAthing()),
});
// Export enhanced Todo Component
export default loadify(connect(mapStateToProps, mapDispatchToProps)(Todo));
I need to change text input value after the state in redux has been changed (re-render the view). That means everything from text input will be stored in redux store (after each character) and re-rendered back to the input. If I use setState without redux, the view is changed successfully but also if I use this.forceUpdate() method for force re-render in handleChange function.
I think that's problem of the fact, that state in reducer isn't changed properly. I googled many way to do it but nothing doesn't worked for me.
Reduced code here:
'use strict';
import * as React from "react";
import { createStore } from 'redux';
interface State
{
}
interface Props
{
}
function cardReducer(state = {redirect : false, cardId : ""}, action) {
console.log("reducer action: " + JSON.stringify(action));
switch (action.type) {
case 'TYPING':
return Object.assign({}, state, {
redirect : false,
cardId : action.cardId
});
default:
return state
}
}
const store = createStore(cardReducer);
console.log("STORE state: " + JSON.stringify(store.getState()));
export class Home extends React.Component<Props, State> {
constructor(props : any)
{
super(props);
}
handleChange = (e) => {
store.dispatch({ type : 'TYPING', cardId : store.getState().cardId + e.target.value});
};
render()
{
return (
<div className={"container cardwrapper offset-md-2 col-md-8"}>
<form>
<input value={store.getState().cardId} onChange={this.handleChange} id={"card_id"} className={"command"} type="text" autoFocus autoComplete={"off"}/>
</form>
);
}
}
}
You're missing connect and mapStateToProps (Google these two). Doing store.getState() in the component is not how React listens for updates.
After getting the data from store.getState(), you need to set it on your state as well for your component to be re rendered.
You can do it as follows:
Put cardId on your state inside the constructor as follows:
this.state {
cardId: ""
}
Then on getting update from store, set the state as:
this.setState({
cardId: store.getState().cardId;
});