React Redux Store updating, but component not re-rendering - reactjs

Using the terminal to test my dispatched actions, Redux-logger shows that my state is being correctly updated. However, my component is not re-rendering as a result of the state change. I've looked at the SO answers regarding component not re-rendering, a majority of the responses claim that the state is being mutated; as a result, Redux won't re-render. However, I'm using Lodash's merge to deep-dup an object, I'm pretty sure I'm not returning a modified object. (Please see attached snippet below)
Would love to hear some advice from you guys, pulling my hair out on this one!
const usersReducer = (state = {}, action) => {
Object.freeze(state); // avoid mutating state
console.log(state);
// returns an empty object
let newState = merge({}, state);
console.log(newState);
// returns my state with my dispatched action object inside already???
// newState for some reason already has new dispatched action
switch (action.type) {
case RECEIVE_USER:
let newUser = {[action.user.id] = action.user};
return merge(newUser, newUser);
case RECEIVE_USERS:
newState = {};
action.users.forEach(user => {
newState[user.id] = user;
});
return merge({}, newState);
default:
return state;
}
};
React Container Component
import { connect } from 'react-redux';
import { receiveUsers, receiveUser, refreshAll, requestUsers, requestUser } from '../../actions/user_actions';
import allUsers from '../../reducers/selectors';
import UserList from './user_list';
const mapStateToProps = (state) => ({
users: allUsers(state), // allUsers (selector that takes the state specfically the user Object and returns an array of user Objects)
state
});
const mapDispatchToProps = (dispatch) => ({
requestUser: () => dispatch(requestUser()),
requestUsers: () => dispatch(requestUsers()),
receiveUsers: (users) => dispatch(receiveUsers(users)),
receiveUser: (user) => dispatch(receiveUser(user)),
refreshAll: (users) => dispatch(refreshAll(users))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserList);
React Presentational component
import React from 'react';
class UserList extends React.Component {
render() {
const { users, state } = this.props;
const userItems = users.map((user, idx) => {
return(<li key={idx}>{user.username}</li>);
});
return (
<div>
<ul>
{ userItems }
</ul>
</div>
);
}
}
export default UserList;
React Store
import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger';
import RootReducer from '../reducers/root_reducer';
const logger = createLogger();
const configureStore = (preloadedState = {}) => {
return createStore(
RootReducer,
preloadedState,
applyMiddleware(logger));
};
// const configureStore = createStore(rootReducer, applyMiddleware(logger));
// oddly enough, when I have the store as a constant and not a function that returns the store constant, dispatching actions through the terminal will correctly update the state and rerender the component
export default configureStore;
React Selector
const allUsers = ({ users }) => {
return Object.keys(users).map(id => (
users[id]
));
};
export default allUsers;

I had a similar problem, just in case someone stumbles upon this, I needed to clone the array in order to re-render the view:
export const addFieldRow = () => (
(dispatch: any, getState: any) => {
const state = getState();
const myArrayOfObjects = myArrayOfObjectsProp(state);
const newObject = {
key: "",
value: "",
};
myArrayOfObjects.push(newObject);
dispatch(addFieldRowAction({ myArrayOfObjects: [...myArrayOfObjects] })); <== here
}
);

Common problem in this case is using not reactive operations for changing state. For example use concat() for array, but not push() and so on.

I use this solution to do it.
I put the users on my state and update it on any change, with componentWillReceiveProps. Hope it helps :-)
class UserList extends React.Component {
constructor(props) {
super(props);
console.log(this.props);
this.state = {
users: props.users
};
}
componentWillReceiveProps(nextProps) {
if (this.props.users !== nextProps.users) {
this.setState({
users: nextProps.users,
});
}
}
render() {
const { users } = this.state;
const userItems = users.map((user, idx) => {
return(<li key={idx}>{user.username}</li>);
});
return (
<div>
<ul>
{ userItems }
</ul>
</div>
);
}
}
export default UserList;

What do your React components look like? Are you using internal state in them or props to push the data down. Usually I see the issue is with people setting the internal state of props with Redux state. You should be pushing props down to the components and they will re-render on update.
Also, check out
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en
to see if the props are really changing.

Create new copy of array from the prop state to re-render the component
render() {
const {allPost} = this.props;
//Use the spread operator to create a new copy of the array
const posts = [...allPost];
const plansList = () => {
return posts.length < 1 ? null : posts && <PlansList allPost={posts}
/>;
};
return (
<>
<Container className="mt-lg-5 pt-lg-5">
{plansList()}
</Container>
</>
);
}

i spent lot of time to find out that when using more than 1 reducer (with combineReducers), then your mapStateToProps should point the correct reducer names, for example
const mapStateToProps = state => ({
someVar: state.yourReducerName.someVar,
loading: state.yourReducerName.loading,
error: state.yourReducerName.error
});

Related

sharing states between two components with useReducer

trying to share the states from one component to another: The state can be accessed from main component but it comes undefined when accessing from a new component
This is my reducer:
export const tableReducer=(state = [], action)=> {
switch (action.type) {
case 'SELECTED_LIST':
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
access it from a different file:
const [userList, usersDispatch] = useReducer(tableReducer, []);
useEffect(() => {
const list = Object.keys(selectedRowIds).length > 0 ? selectedFlatRows.map(
d => d.original.email
)
: '';
usersDispatch({ type: 'SELECTED_LIST', payload: list, });
}, [selectedRowIds, selectedFlatRows]);
and in a new component:
const [userList] = useReducer(tableReducer);
const deleteUsers = () => {
console.log(userList)
}
but here console.log(userList) it results to undefined
For Sharing of state between components, you can use Context API with useReducer.
Context API provides a neat way of providing state to child components without ending up with a prop drilling situation. It requires that a Provider is setup, which provides its values to any of its Consumers. Any component that is a child of the Provider can consume the context.
First a piece of context is created.
CustomContext.js
import React from 'react';
const CustomContext = React.createContext();
export function useCustomContext() {
return React.useContext(CustomContext);
}
export default CustomContext;
We can define your reducer in a seperate file.
TableReducer.js
export const tableReducer=(state = [], action)=> {
switch (action.type) {
case 'SELECTED_LIST':
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
next is to implement the provider, and give it a value within a "Parent" component (A higher up component)
Parent.js
import CustomContext from './CustomContext'
import { tableReducer } from './TableReducer'
const ParentComponent = () => {
const [userState, usersDispatch ] = React.useReducer(tableReducer, []);
const providerState = {
userState,
usersDispatch
}
return (
<CustomContext.Provider value={providerState} >
<ChildComponent /> //Any component within here can access value by using useCustomContext();
</CustomContext.Provider>
)
}
now any component nested within <CustomContext.Provider></CustomContext.Provider> can access whatever is passed into "value" prop of the Provider which is your context state and the dispatch method.
The child component will look like this (I have ommited your state values and such..)
Child.js
import { useCustomContext }from './CustomContext'
const ChildComponent = (props) => {
//your custom state variables and other methods
const { userState, usersDispatch } = useCustomContext();
useEffect(() => {
const list = Object.keys(selectedRowIds).length > 0 ? selectedFlatRows.map(
d => d.original.email
)
: '';
usersDispatch({ type: 'SELECTED_LIST', payload: list, });
}, [selectedRowIds, selectedFlatRows]);
return(
<div>your components dependent on selectedRowIds, selectedFlatRows<div>
)
}
You can't share the state with useReducer hook like you are trying to. Each call to useReducer returns a new state that is managed using the reducer function passed to useReducer hook.
Just as each call to useState returns a different state, each call to useReducer returns a different state. Two useReducer calls can't share the same state.
To share the state, you can use one of the following options:
Context API
React-Redux
Pass the state from parent component to child component using props
#Gandzal is correct but I found it was lacking a typscript version and also today createContext requieres a default parameter. This came up as one of the top answers on google so I thought I would share.
I setup my solution like this:
Custom context:
import React, {Dispatch} from 'react';
import {StateType, Action} from './reducer'
interface IContextProps {
state: StateType;
dispatch:Dispatch<Action>
}
const CustomContext = React.createContext({} as IContextProps);
export function useCustomContext() {
return React.useContext(CustomContext);
}
export default CustomContext;
Note StateType and Action:
export type StateType = {
items: Array<DataItems>;
date: Date;
};
export type Action = {
type: ActionKind;
payload: DataItems;
};
reducer:
export const reducer = (state: StateType, action: Action) => {
const { type, payload } = action;
let newArray: Array<DataItems> = [];
switch (type) {
case ActionKind.Checked:
newArray = state.items.map((item) => ({
...item,
checked: item.id === payload.id ? true : item.checked,
}));
return {
...state,
items: newArray,
}
default:
return state;
}
};
App.tsx:
import { reducer, initalState } from 'Shared/Reducer/reducer';
import CustomContext from 'Shared/Reducer/CustomContext';
const App: React.FC = () => {
const [state, dispatch] = React.useReducer(reducer, initalState);
const providerState = {
state,
dispatch,
};
return (
<CustomContext.Provider value={providerState}>
<main role="main">
// your components
</main>
</CustomContext.Provider>
);
};
export default App;
And one of your components:
import { useCustomContext } from 'Shared/Reducer/CustomContext';
export const MyComp: React.FC<MyType> = (props) => {
const { data} = props;
const { state, dispatch } = useCustomContext(); --- Your state and dispatch here
return (
<div>
// your component
</div >
);
}

React-Redux State won't update onClick

New to React. I am trying out react redux for the first time (on my own). I have a state for a gameboard called force_hidden that I want to set in App.js and then use in a child component ( a few levels down). I used redux to create forceGameBoardHidden that should set force_hidden to whatever value is inside the (). so, forceGameBoardHidden(true) should set the state of force_hidden to true. However, that doesn't happen. I can click on the item and it logs "before change" and then the state. In between it should have set the state to true, but the state is still false. I don't know what's going wrong here. I tried console.logging the gameBoardReducer. It fires when I start the page, but doesn't fire when I click the button.
gameboard.types.js
const GameBoardActionTypes = {
FORCE_GAMEBOARD_HIDDEN: 'FORCE_GAMEBOARD_HIDDEN'
}
export default GameBoardActionTypes;
gameboard.action.js
import GameBoardActionTypes from './game-board.types';
export const forceGameBoardHidden = value => ({
type: GameBoardActionTypes.FORCE_GAMEBOARD_HIDDEN,
payload: value
});
gameboard.reducer.js
import GameBoardActionTypes from './game-board.types'
const INITIAL_STATE = {
force_hidden: false
}
const gameBoardReducer = ( state = INITIAL_STATE, action) => {
switch (action.type) {
case GameBoardActionTypes.FORCE_GAMEBOARD_HIDDEN:
return {
...state,
force_hidden: action.payload
}
default:
return state;
}
}
export default gameBoardReducer;
root-reducer
import { combineReducers } from 'redux';
import gameBoardReducer from './game-board/game-board.reducer'
export default combineReducers ({
gameboard: gameBoardReducer
})
store.js
const middlewares = [];
const store = createStore(rootReducer, applyMiddleware(...middlewares))
export default store;
index.js
<Provider store={store}>
App.js -- this is where the magic should happen in forceGameBoardHidden
const App = () => {
const handleKeyChange = event => {
setKey(event.target.value);
console.log("before change")
forceGameBoardHidden(true)
console.log(store.getState().gameboard)
}
return (
<SearchBox
onChange={handleKeyChange}
placeholder="Enter your game Key"/>
</div>
);
}
const mapDispatchToProps = dispatch => ({
forceGameBoardHidden: item => dispatch(forceGameBoardHidden(item))
})
export default connect(null,mapDispatchToProps)(App);
I think you need to dispatch the action, there are 2 methods , one is to connect the component to the actions and bind them to dispatch. The other one is much easier since you use functional components, is by using the useDispatch hook
Example here:
import { useDispatch } from 'react-redux' // <-- add this
const App = () => {
const dispatch = useDispatch() // <-- add this
const handleKeyChange = event => {
setKey(event.target.value);
console.log("before change")
dispatch(forceGameBoardHidden(true)) // <-- change this
console.log(store.getState().gameboard)
}
return (
<SearchBox
onChange={handleKeyChange}
placeholder="Enter your game Key"/>
</div>
);
}

Redux api calling

I'm wanting to update my trending array with the results calling the tmdb api. I'm not sure if im going about this the wrong way with calling the api or if im messing up somewhere else along the way. So far I've really been going in circles with what ive tried. Repeating the same things and not coming to a real solution. Havent been able to find another question similar to mine.
my actions
export const getTrending = url => dispatch => {
console.log("trending action");
axios.get(url).then(res =>
dispatch({
type: "TRENDING",
payload: res.data
})
);
};
my reducer
const INITIAL_STATE = {
results: [],
trending: []
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case "SEARCH_INFO":
return {
results: [action.payload]
};
case "TRENDING":
return { trending: action.payload };
default:
return state;
}
};
and my component im trying to get the results from
import React, { Component } from "react";
import Trending from "./Treding";
import "../App.css";
import { getTrending } from "../actions/index";
import { connect } from "react-redux";
export class Sidebar extends Component {
componentDidMount = () => {
const proxy = `https://cors-anywhere.herokuapp.com/`;
getTrending(`${proxy}https://api.themoviedb.org/3/trending/all/day?api_key=53fbbb11b66907711709a6f1e90fc884
`);
};
render() {
return (
<div>
<h3 className="trending">Trending</h3>
{
this.props.trending ? (
<Trending movies={this.props.trending} />
) : (
<div>Loading</div>
)}
</div>
);
}
}
const mapStateToProps = state => {
return {
trending: state.trending
};
};
export default connect(mapStateToProps)(Sidebar);
Since you are directly calling the getTrending without passing it to connect method, it might be the issue.
Instead that you can pass getTrending to connect method so it can be available as props in the component. After that it can be dispatched and it will be handled by redux/ redux-thunk.
export default connect(mapStateToProps, { getTrending })(Sidebar);
And access it as props in the component.
componentDidMount = () => {
// const proxy = `https://cors-anywhere.herokuapp.com/`;
this.props.getTrending(`https://api.themoviedb.org/3/trending/all/day?api_key=53fbbb11b66907711709a6f1e90fc884
`);
};

How to access redux-store from within react's componentDIdMount()

In the following code I am trying to pass the state.userData.userDetails from the redux-store to getleftpaneProductCatalogue(), but state.userData.userDetails is unaccessible to componentDidMount(). I tried assigning the state.userData.userDetails to this.prop.userProfile, but still this.prop.userProfile is an empty value. How to access the prop within componentDidMount?
import React,{Component} from 'react';
import { connect } from 'react-redux';
import {Row, Col } from 'react-materialize';
import {getleftpaneProductCatalogue} from '../actions/leftpane-actions';
import ProductCatalogueLeftPaneComp from '../components/pages/product-catalogue-leftpane';
class ProductCatalogueLeftPane extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
console.log('this.props^', JSON.stringify(this.props));
this.props.getleftpaneProductCatalogue().then((data) => {
console.log('productdata', data);
})
}
render() {
return (
<div>
{JSON.stringify(this.props.userProfile)}
</div>
)
}
}
const mapStateToProps = (state) => {
console.log('state^', JSON.stringify(state));
return {leftpaneProductCatalogue: state.leftpaneProductCatalogue, userProfile: state.userData.userDetails};
};
const mapDispatchToProps = (dispatch) => {
return {
getleftpaneProductCatalogue: () => dispatch(getleftpaneProductCatalogue()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ProductCatalogueLeftPane);
You can access the state directly in mapDispatchToProps and pass it to getleftpaneProductCatalogue:
componentDidMount() {
const { dispatch, getleftpaneProductCatalogue }
dispatch(getleftpaneProductCatalogue())
}
const mapDispatchToProps = dispatch => {
return {
getleftpaneProductCatalogue: () => (dispatch, getState) => {
const state = getState()
const details = state.userData.userDetails
return dispatch(getleftpaneProductCatalogue(details))
},
dispatch
}
}
However, the way you're doing it, passing the state via mapStateToProps is still valid, but more verbose. Therefore the problem would be somewhere else.
Here's my bet. I guess you're getting the userData somewhere in your code with async API call and it's not being fetched yet. If that's the case - then you should wait for data being fetched firstly, then you can access it in your component ProductCatalogueLeftPane.

Simple React-Redux component not updating

I'm just starting to learn redux with react and I've been having this simple issue for a while now that I can't figure out. I have a button that dispatches an action to increment a value, and the action goes through and updates the state, but no change is ever reflected in the component. What am I doing wrong here?
const ACTION = 'ACTION';
const defaultState = {
value: 5
};
const doAction = ()=>{
return {
type: ACTION
};
};
const reducer = (state = defaultState, action) => {
let update = Object.assign({}, state);
switch(action.type){
case ACTION:
update.value = state.value + 1;
return update;
default:
return state;
}
};
const store = Redux.createStore(reducer);
class App extends React.Component {
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e){
this.props.doAction();
}
render(){
return(
<div id='app'>
<button onClick={this.handleClick}>Click</button>
<p>{this.props.value}</p>
</div>
);
}
}
const Provider = ReactRedux.Provider;
const mapStateToProps = (state)=> {
return {
value: state.value
};
}
const mapDispatchToProps = (dispatch) => {
return {
doAction: () => {
dispatch(doAction())
}
};
}
const Container = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App);
class AppWrapper extends React.Component {
constructor(props){
super(props);
}
render(){
return(
<Provider store={store}>
<Container />
</Provider>
);
}
}
ReactDOM.render(<AppWrapper />, document.getElementById('root'));
Issue regarding codepen.io
Codepen provides the newest version of react-redux by default, unluckily it is a test version (5.1.0-test.1) currently. Change the external script url of react-redux to https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.7/react-redux.js and your pen should be working: Working example
I don't recommend codepen for react development. Consider create-react-app or codesandbox.io if it has to be an online ide. You really need more than one editable file to structure your actions, reducers and components.
How to improve your code
In your reducer you create a copy of the previous state in every case, even when you return the previous state. Try this instead:
const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTION:
return { ...state, value: state.value + 1 };
// Alternative: Object.assign({}, state, { value: state.value + 1 });
default:
return state;
}
};
This will ensure you create objects only if needed. You could have returned { value: state.value + 1 } for this simple state shape, the spread operator or Object.assign(...) are a valuable tool when you want to clone the previous state and change only some (or delete) properties without affecting others.
Furthermore you can refactor your app to a functional react component:
const App = (props) => (
<div>
<button onClick={props.action}>Click</button>
<p>{props.value}</p>
</div>
);
And connect the App to redux with:
const mapStateToProps = (state) => ({
value: state.value
});
const mapDispatchToProps = dispatch => ({
action: () => dispatch(doAction())
});
const Container = connect(mapStateToProps, mapDispatchToProps)(App);
Note that i stick to your component names here.
Where to go from here...
If you are interested in a bigger example i have created this sandbox which features routing, fake authentication, a todo list and a simple counter. Redux devtools is integrated, too. Just open the browser extension on this site to inspect the store and the dispatched actions.

Resources