Why after update store, component doesn't change (React-Redux) - javascript

I have a component, that doesn't change data after updated store
class RenderComments extends Component {
commentsParse() {
return this.props.comments.map((comment) => {
if (comment.hasComments === true) {
return (
<div key={comment.id}>
<CommentItem data={comment} />
</div>
) // return
}
}); // this.props.comments.map()
} // commentParse
render() {
return (
<div>
{ this.commentsParse() }
</div>
) // return
} // render
} // RenderComments
function mapStateToProps(state) {
return {
comments: state
}
}
export default connect(mapStateToProps)(RenderComments);
dispatcher
class AddComment extends Component {
constructor(props) {
super(props);
this.state = {
comment: ''
}
}
addNewComment() {
this.props.addNewComment(this.state);
}
render() {
return (
<form>
<div className="comment-entry">
<div className="form-group">
<textarea
onChange={event => this.setState({comment: event.target.value})}
>
</textarea>
</div>
<div className="comment-entry-footer">
<button
onClick={() => this.addNewComment()}
>
Submit
</button>
</div>
</div>
</form>
) // return
} // render
} // AddComment
function mapStateToProps(state) {
return {
comment: state
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({addNewComment}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(AddComment);
this is my reducer
import { ADD_COMMENT } from './../constants';
let allUserComments = [/* Array with 4 objects */]
const createNewComment = (action) => {
let d = new Date();
let n = d.toLocaleTimeString();
let newComment = {
id: Math.random(),
rating: 0,
name: 'MyNick',
thumbnail: 'icon.50x50.png',
time: n,
comment: action.text.comment,
replyTo: null,
replyToUser: null,
hasComments: false
}
allUserComments.push(newComment);
return allUserComments;
}
export default (state = allUserComments, action) => {
switch (action.type) {
case ADD_COMMENT:
allUserComments = createNewComment(action);
return allUserComments;
default:
return state;
}
}
In first launch application, my component shows 4 objects i.e state that return reducer, but after action, component doesn't re-render, it's continuing shows 4 object instead 5. Why component doesn't update?

Your reducer is very wrong, on several levels. It's not following the requirement of updating data immutably.
First, you shouldn't be maintaining a separate allUserComments array outside the reducer function.
Second, you shouldn't be using .push() to mutate an array - you would want to use .concat() to return a new array that also contains the added item.
Third, you're also generating a new ID value in a reducer.
None of those follow the principle of "pure functions" and immutable updates in reducers. Since you're directly mutating state and keeping the same array reference, that will almost definitely keep your components from updating properly.
Please see the Redux FAQ entry on mutations keeping components from rendering for more info, as well as the Structuring Reducers - Immutable Update Patterns section.

Related

useEffect in React not update component after update store

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)

Redux: API data response sorted on click

I am trying to develop an application, that is showing photos from Unsplash given a keyword. I managed to fetch specific photos using unsplash.js:
actions:
export function fetchPhotos(term) {
const unsplash = new Unsplash({
applicationId:
"id",
secret: "secret",
callbackUrl: "callback"
});
const response = unsplash.search
.photos(term, 1, 20)
.then(toJson)
.then(json => json);
return {
type: FETCH_PHOTOS,
payload: response
};
}
export function setCategory(term) {
return {
type: SET_CATEGORY,
categories: [term]
};
}
export function sortPhotos(attribute) {
return {
type: SORT_PHOTOS,
attribute
}
}
Component that renders the photos:
import React, { Component } from "react";
import { connect } from "react-redux";
import SinglePhoto from "../components/SinglePhoto";
class PhotoList extends Component {
renderPhotos() {
const { photos } = this.props;
console.log(photos);
if (!photos) {
return <p>Loading...</p>;
}
return photos.map(photo => {
const url = photo.urls.full;
const id = photo.id;
const alt = photo.description;
return <SinglePhoto url={url} key={id} alt={alt} />;
});
}
render() {
return <div>{this.renderPhotos()}</div>;
}
}
function mapStateToProps(state) {
return {
photos: state.photos,
categories: state.categories
};
}
export default connect(mapStateToProps)(PhotoList);
And reducers:
import { FETCH_PHOTOS, SORT_PHOTOS } from "../actions/types";
export default function(state = [], action) {
switch (action.type) {
case FETCH_PHOTOS:
return [...action.payload.results];
case SORT_PHOTOS:
break;
default:
return state;
}
}
What I am struggling to do is to actually sort the array of data I receive from the API according to a specific term. The response is an array of objects that makes it impossible to call it in an external component I've called Buttons that I have wanted to set the logic in:
class Buttons extends Component {
render() {
const { created_at: date } = this.props.photos;
console.log(this.props);
return (
<div className="buttons">
{/* <button onClick={() => this.props.sortPhotos(date)}>Sort by creation date</button> */}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
photos: state.photos
}
}
const mapDispatchToProps = (dispatch) => bindActionCreators({sortPhotos}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Buttons);
As I would need to loop over the photos to actually receive their created_at props.
I would like to sort them, for example, taking created_at into account. This would be handled by a button click (there would be other buttons for let's say likes amount and so on). I tried to do this in mapStateToProps until the moment I realized it would be impossible to call this with onClick handler.
As I have read this post, I thought it would be a great idea, however, I am not sure, how can I handle this request by an action creator.
Is there any way that I could call sorting function with an onclick handler?
One approach you can take is using a library such as Redux's reduxjs/reselect to compute derived data based on state, in this case sorted items based on some object key and/or direction. Selectors are composable and are usually efficient as they are not recomputed unless one of its arguments changes. This approach is adding properties to the reducer's state for sort key and sort order. As these are updated in the store via actions/reducers, the selector uses state to derive the elements in the resulting sorted order. You can utilize the sorted items in any connected component.
I've tried my best to recreate a complete example including actions, reducers, selectors, and store structure.
Actions - Created actions for setting sort key/direction. My example is using redux-thunk for handling async actions, but that is in no way necessary:
export const SET_SORT = 'SET_SORT';
const setSort = (sortDirection, sortKey) => ({
type: SET_SORT,
sortDirection,
sortKey
});
export const sort = (sortDirection = 'desc', sortKey = 'created_at') => dispatch => {
dispatch(setSort(sortDirection, sortKey));
return Promise.resolve();
};
Reducer - Updated initial state to keep track of a sort key and/or sort direction with photo objects being stored in a child property such as items:
const initialState = {
isFetching: false,
sortDirection: null,
sortKey: null,
items: []
};
const photos = (state = initialState, action) => {
switch (action.type) {
case FETCH_PHOTOS:
return {
...state,
isFetching: true
};
case RECEIVE_PHOTOS:
return {
...state,
isFetching: false,
items: action.photos
};
case SET_SORT:
return {
...state,
sortKey: action.sortKey,
sortDirection: action.sortDirection
};
default:
return state;
}
};
Selector - Using reselect, create selectors that retrieves items/photos, sortOrder, and sortDirection. The sorting logic can obviously be enhanced to handle other keys/conditions/etc:
import { createSelector } from 'reselect';
const getPhotosSelector = state => state.photos.items;
const getSortKeySelector = state => state.photos.sortKey;
const getSortDirectionSelector = state => state.photos.sortDirection;
export const getSortedPhotosSelector = createSelector(
getPhotosSelector,
getSortKeySelector,
getSortDirectionSelector,
(photos, sortKey, sortDirection) => {
if (sortKey === 'created_at' && sortDirection === 'asc') {
return photos.slice().sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
} else if (sortKey === 'created_at' && sortDirection === 'desc') {
return photos.slice().sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
} else {
return photos;
}
}
);
Component - Utilize selector to render items. Trigger dispatch of sort action via button click passing in a sort key and/or sort order. The linked example uses dropdowns in combination with the button click to set sort key/order:
import { getSortedPhotosSelector } from './selectors';
// ...
handleClick() {
this.props.dispatch(sort('desc', 'created_at'));
}
render() {
const { sortDirection, sortKey, items } = this.props;
<ul>
{items.map(item => <li key={item.id}>{item.created_at}</li>)}
</ul>
<button type="button" onClick={this.handleClick}>SORT</button>
}
const mapStateToProps = state => ({
items: getSortedPhotosSelector(state),
sortKey: state.photos.sortKey,
sortDirection: state.photos.sortDirection
});
export default connect(mapStateToProps)(PhotoList);
Here is a StackBlitz, demonstrating the functionality in action. It includes controlled components such as and to trigger dispatch of a sort action.
Hopefully that helps!

React Context API and avoiding re-renders

I have updated this with an update at the bottom
Is there a way to maintain a monolithic root state (like Redux) with multiple Context API Consumers working on their own part of their Provider value without triggering a re-render on every isolated change?
Having already read through this related question and tried some variations to test out some of the insights provided there, I am still confused about how to avoid re-renders.
Complete code is below and online here: https://codesandbox.io/s/504qzw02nl
The issue is that according to devtools, every component sees an "update" (a re-render), even though SectionB is the only component that sees any render changes and even though b is the only part of the state tree that changes. I've tried this with functional components and with PureComponent and see the same render thrashing.
Because nothing is being passed as props (at the component level) I can't see how to detect or prevent this. In this case, I am passing the entire app state into the provider, but I've also tried passing in fragments of the state tree and see the same problem. Clearly, I am doing something very wrong.
import React, { Component, createContext } from 'react';
const defaultState = {
a: { x: 1, y: 2, z: 3 },
b: { x: 4, y: 5, z: 6 },
incrementBX: () => { }
};
let Context = createContext(defaultState);
class App extends Component {
constructor(...args) {
super(...args);
this.state = {
...defaultState,
incrementBX: this.incrementBX.bind(this)
}
}
incrementBX() {
let { b } = this.state;
let newB = { ...b, x: b.x + 1 };
this.setState({ b: newB });
}
render() {
return (
<Context.Provider value={this.state}>
<SectionA />
<SectionB />
<SectionC />
</Context.Provider>
);
}
}
export default App;
class SectionA extends Component {
render() {
return (<Context.Consumer>{
({ a }) => <div>{a.x}</div>
}</Context.Consumer>);
}
}
class SectionB extends Component {
render() {
return (<Context.Consumer>{
({ b }) => <div>{b.x}</div>
}</Context.Consumer>);
}
}
class SectionC extends Component {
render() {
return (<Context.Consumer>{
({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
}</Context.Consumer>);
}
}
Edit: I understand that there may be a bug in the way react-devtools detects or displays re-renders. I've expanded on my code above in a way that displays the problem. I now cannot tell if what I am doing is actually causing re-renders or not. Based on what I've read from Dan Abramov, I think I'm using Provider and Consumer correctly, but I cannot definitively tell if that's true. I welcome any insights.
There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.
Create your reducer
export const initialState = {
...
};
export const reducer = (state, action) => {
...
};
Create your ContextProvider component
export const AppContext = React.createContext({someDefaultValue})
export function ContextProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState)
const context = {
someValue: state.someValue,
someOtherValue: state.someOtherValue,
setSomeValue: input => dispatch('something'),
}
return (
<AppContext.Provider value={context}>
{props.children}
</AppContext.Provider>
);
}
Use your ContextProvider at top level of your App, or where you want it
function App(props) {
...
return(
<AppContext>
...
</AppContext>
)
}
Write components as pure functional component
This way they will only re-render when those specific dependencies update with new values
const MyComponent = React.memo(({
somePropFromContext,
setSomePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext,
}) => {
... // regular component logic
return(
... // regular component return
)
});
Have a function to select props from context (like redux map...)
function select(){
const { someValue, otherValue, setSomeValue } = useContext(AppContext);
return {
somePropFromContext: someValue,
setSomePropFromContext: setSomeValue,
otherPropFromContext: otherValue,
}
}
Write a connectToContext HOC
function connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
Put it all together
import connectToContext from ...
import AppContext from ...
const MyComponent = React.memo(...
...
)
function select(){
...
}
export default connectToContext(MyComponent, select)
Usage
<MyComponent someRegularPropNotFromContext={something} />
//inside MyComponent:
...
<button onClick={input => setSomeValueFromContext(input)}>...
...
Demo that I did on other StackOverflow question
Demo on codesandbox
The re-render avoided
MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there.
The code inside select will run every time any value from context updates, but it does nothing and is cheap.
Other solutions
I suggest check this out Preventing rerenders with React.memo and useContext hook.
I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated.
Check it out below, or try the CodeSandbox
index.tsx
import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'
function useConsume(prop: 'lang' | 'count') {
const contextState = useState()
const [state, setState] = React.useState(contextState[prop])
const listener = (e: CustomEvent) => {
if (e.detail && prop in e.detail) {
setState(e.detail[prop])
}
}
React.useEffect(() => {
document.addEventListener('update', listener)
return () => {
document.removeEventListener('update', listener)
}
}, [state])
return state
}
function CountDisplay() {
const count = useConsume('count')
console.log('CountDisplay()', count)
return (
<div>
{`The current count is ${count}`}
<br />
</div>
)
}
function LangDisplay() {
const lang = useConsume('lang')
console.log('LangDisplay()', lang)
return <div>{`The lang count is ${lang}`}</div>
}
function Counter() {
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({type: 'increment'})}>
Increment count
</button>
)
}
function ChangeLang() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}
function App() {
return (
<CountProvider>
<CountDisplay />
<LangDisplay />
<Counter />
<ChangeLang />
</CountProvider>
)
}
const rootElement = document.getElementById('root')
render(<App />, rootElement)
count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {...state, count: state.count + 1}
}
case 'switch': {
return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {
count: 0,
lang: 'en',
})
const stateRef = React.useRef(state)
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {count: state.count},
})
document.dispatchEvent(customEvent)
}, [state.count])
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {lang: state.lang},
})
document.dispatchEvent(customEvent)
}, [state.lang])
return (
<CountStateContext.Provider value={stateRef.current}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function useDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useDispatch must be used within a AccountProvider')
}
return context
}
export {CountProvider, useState, useDispatch}
To my understanding, the context API is not meant to avoid re-render but is more like Redux. If you wish to avoid re-render, perhaps looks into PureComponent or lifecycle hook shouldComponentUpdate.
Here is a great link to improve performance, you can apply the same to the context API too

React-Redux Transform data to props

I'm implementing a Task App where I have two views to render Tasks and Items and one where I render multiple lists based on the Task Status as kanban board.
My reducer:
export const rootReducer = Redux.combineReducers({
Tasks: TasksReducer,
itemsAreLoading: itemsAreLoadingReducer
});
const TasksReducer = (state , action ) => {
if (typeof state == "undefined") {
console.log('state undefined');
return null;
}
switch (action.type) {
case TasksTypes.Tasks_GET:
return action.Tasks;
default:
console.log(state);
return state;
}
}
export class TasksApp extends React.Component {
constructor(props) {
super(props);
}
render() {
const {tasks} = this.props;
return (<div>
<ItemsView Tasks={tasks}/>
<BoardView Lanes=[/* tasks tranfromed into mutliple list based on their status*/]/>
</div>);
}
}
const mapStateToProps = (state) => {
return {
tasks: state.Tasks
};
};
My Question is where to transform the data for the second view to have a different representation of the data.
The main problem here is that you dont fire any actions in your class, and I dont see any actions here neither. So first, you have to fire an action, and dispatch it with the type and payload, second, as David Tyron wrote, the syntax was a bit off in this line:
const { tasks } = this.props;
And for the end a small remark, you can do some destruction in the mapStateToProps function:
const mapStateToProps = ({ Tasks }) => {
return { Tasks };
};
And then get it like const { Tasks } = this.props;
I think, that the best practice to change your tasks props is to fire another action that creates a new props from your tasks props, something like:
export const transformData = tasks => {
return dispatch => {
//Do the transformations here
dispatch {
type: TRANSFORM_DATA,
payload: transformed_tasks
}
}
}
And then catch it with a reducer.
And IMHO, the best place to call this action is the componentDidMount()

shouldComponentUpdate + deepEqual and arrays

Problem: shouldComponentUpdate retrieves previous state with this.state, that doesn't work if you keep reference to array at UserList, and update array entity at UserStore.
PureRenderMixin.js
const deepEqual = require('deep-equal');
module.exports = function pureRenderMixin(Component) {
Component.prototype.shouldComponentUpdate = function(nextProps, nextState) {
return !deepEqual(this.props, nextProps) || !deepEqual(this.state, nextState);
};
return Component;
};
UserList.react.js
class UserList extends React.Component {
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
}
componentWillMount() {
UsersStore.addChangeListener(this._onChange);
}
_onChange() {
this.setState({userList: UsersStore.getState()});
}
}
module.exports = PureRenderMixin(UserList);
UsersStore.js
......
getState() { return _userList; }
switch(action.type) {
case ActionTypes.UPDATE_USER_FLAG:
//!!!!!!!!!!!!!!
//PROBLEM: since UserList.react keep userList reference, there is no way to retrieve previous state inside shouldComponentUpdate
_userList[action.index].flag = action.flag;
UsersStore.emitChange();
break;
}
#taggon solution
thanks to taggon, now I know how to make shouldComponentUpdate keep the reference to previous state:
UsersStore.js
......
getState() { return _userList; }
switch(action.type) {
case ActionTypes.UPDATE_USER_FLAG:
//SOLUTION: copy an array, so there will be two versions of _userList[action.index]
_userList = _.map(_userList, _.clone);
_userList[action.index].flag = action.flag;
UsersStore.emitChange();
break;
}
I think the problem is in the store.
The store would be better to create another array whenever its state is changed.
For example, think the store as an array:
var store = [ ];
export default store;
You may want to write add() function like this:
export function add(newItem) {
store = [ ...store, newItem ];
// or write this in es5
store = store.concat([newItem]);
// trigger change event or dispatch an action here
}
Likewise, remove() function can be:
export remove(index) {
store = [ ...store.slice(0, index), ...store.slice(index+1) ];
// trigger change event or dispatch an action here
}
In this way, the store dereference the component's state whenever the store is changed. This makesshouldComponentUpdate() return true.
I hope this helps you.
if your props are immutable, you can compare the data by reference safely and easily. you can have a look at immutablejs
class ProductStore extends ReduceStore {
getInitialState() {
return Immutable.OrderedMap({1: new Product('react', 'flux'), 2: new Product('angular', 'mvc')});
}
reduce (state, action) {
switch (action.type) {
case 'product/item-selected':
return state.map((product)=> {
return product.set('selected', product.id === action.id);
});
case 'product/search':
let alldata = this.getInitialState();
return alldata.filter((product)=> {
return product.name.indexOf(action.value) !== -1;
});
default:
return state;
}
}
}
export default class ProductDetail extends Component {
shouldComponentUpdate(nextProps) {
return this.props.product !== nextProps.product;
}
render() {
const {product} = this.props;
return (
<div className="product-detail">
<div className="product-name">
{product.name}
</div>
<div className="product-type">
{product.type}
</div>
</div>
);
}
}
https://facebook.github.io/immutable-js/

Categories