Edit: the bug was is a separated helper function that was mutating the state (not displayed in the post).
I'm experimenting with ReactDnD to create a sortable image grid via drag and drop. I've been following this tutorial 1 and trying to implement it with redux instead of React Context.
The issue that I'm having is that my props don't get updated after I re-arrange the images. I have been debugging the reducers and noticed that the state gets somehow updated before the reducer has the chance to do so (which would trigger mapStateToProps to reload my component with the updated state). The problem though it that I have no idea why that happens. I have the feeling that since ReactDnD is also using Redux, it's somehow causing this.
Here are the different parts:
Index.js
export const store = createStore(reducers, applyMiddleware(thunk))
ReactDOM.render(
<Provider store={store}>
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>
</Provider>,
document.getElementById('root')
)
App.js (parent component of DroppableCell and DraggableItem)
class App extends React.Component {
componentDidMount() {
this.props.loadCollection(imageArray)
}
render() {
return (
<div className='App'>
<div className='grid'>
{this.props.items.map((item) => (
<DroppableCell
key={item.id}
id={item.id}
onMouseDrop={this.props.moveItem}
>
<DraggableItem src={item.src} alt={item.name} id={item.id} />
</DroppableCell>
))}
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return { items: state.items }
}
export default connect(mapStateToProps, {
moveItem,
loadCollection,
})(App)
DroppableCell (calling the action creator from parent component)
import React from 'react'
import { useDrop } from 'react-dnd'
const DroppableCell = (props) => {
const [, drop] = useDrop({
accept: 'IMG',
drop: (hoveredOverItem) => {
console.log(hoveredOverItem)
props.onMouseDrop(hoveredOverItem.id, props.id)
},
})
return <div ref={drop}>{props.children}</div>
}
export default DroppableCell
DraggableItem
import React from 'react'
import { useDrag } from 'react-dnd'
const DraggableItem = (props) => {
const [, drag] = useDrag({
item: { id: props.id, type: 'IMG' },
})
return (
<div className='image-container' ref={drag}>
<img src={props.src} alt={props.name} />
</div>
)
}
export default DraggableItem
Reducer
import { combineReducers } from 'redux'
const collectionReducer = (state = [], action) => {
// state is already updated before the reducer has been run
console.log('state:', state, 'action: ', action)
switch (action.type) {
case 'LOAD_ITEMS':
return action.payload
case 'MOVE_ITEM':
return action.payload
default:
return state
}
}
export default combineReducers({
items: collectionReducer,
})
The action creator
export const moveItem = (sourceId, destinationId) => (dispatch, getState) => {
const itemArray = getState().items
const sourceIndex = itemArray.findIndex((item) => item.id === sourceId)
const destinationIndex = itemArray.findIndex(
(item) => item.id === destinationId
)
const offset = destinationIndex - sourceIndex
//rearrange the array
const newItems = moveElement(itemArray, sourceIndex, offset)
dispatch({ type: 'MOVE_ITEM', payload: newItems })
}
found the bug - unfortunately was outside the code posted as I thought it was a simple helper function. I realised I was using the 'splice' method to rearrange the imageArray, and therefore mutating the state.
Related
I'm learning redux, and i've a method addPosts to add posts to the list of posts, and I'm doing it like this.
import { createSlice } from "#reduxjs/toolkit";
var initialState = [{ number: 1 }, { number: 2 }, { number: 3 }, { number: 4 }];
export const postsSlice = createSlice({
name: "postsSlice",
initialState,
reducers: {
addPost: (state, action) => {
state = [...state, action.payload];
},
},
});
export const allPosts = (state) => state.posts;
export const { addPost } = postsSlice.actions;
export default postsSlice.reducer;
and using the state like this.
import { useSelector, useDispatch } from "react-redux";
import { addPost, allPosts } from "./postsSlice";
function Posts() {
var posts = useSelector(allPosts);
var dispatch = useDispatch();
return (
<div>
{posts.map((post) => (
<div>{post.number}</div>
))}
{/* add post */}
<button
onClick={() => {
dispatch(addPost({ number: 1 }));
console.log(posts);
}}
>
addpost
</button>
</div>
);
}
export default Posts;
using state.push(action.payload) works somehow, altough the documentation says not use update state like this, and update in an immutable way.
like this state = [...state, action.payload]. it does not update state with this immutable way.
I don't know what is wrong that i'm doing.
thanks in advance for any help
You are misreading the wrong documentation for the wrong tool it seems - in a Redux Toolkit createSlice reducer, it is always 100% correct to use something like state.push to mutably modify the object in the state variable.
What you cannot do however is what you are trying here: reassign the state variable. That had never any effect in any kind of Redux reducer, unless you would return that state variable later.
If you want to do that, you will need to return [...state, action.payload] instead and leave the state variable alone altogether - it should not be reassigned.
But the recommended way would be that push.
For more, please read Writing Reducers with Immer
As per this instead of directly changing into state you can return in this way
return [...state, action.payload]
Depending on your definition of initialState
Please have a look into working example of react-redux-toolkit-slice-example
Below is the definition of slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = [{ number: 1 }];
export const postsSlice = createSlice({
name: "postsSlice",
initialState,
reducers: {
addPost: (state, action) => {
return [...state, action.payload];
}
}
});
export const allPosts = (state) => state.posts || [];
export const { addPost } = postsSlice.actions;
export default postsSlice.reducer;
Defining the reducer(postSlice) in store
import { configureStore } from "#reduxjs/toolkit";
import postsReducer from "../features/posts/postsSlice";
export default configureStore({
reducer: {
posts: postsReducer
}
});
Use of slice in component
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { addPost, allPosts } from "./postsSlice";
const Posts = () => {
var posts = useSelector(allPosts);
var dispatch = useDispatch();
return (
<div>
{posts.map((post, key) => (
<div key={key}>{post.number}</div>
))}
{/* add post */}
<button
onClick={() => {
dispatch(
addPost({
number: Math.max(...posts.map(({ number }) => number)) + 1
})
);
console.log(posts);
}}
>
Add Post
</button>
</div>
);
};
export default Posts;
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
without redux it works so that not a api connection problem
I have an express app connected to react with proxy I have already managed to display my data in react but now i want to make that in redux soo:
There is my problem, i have maked all the reducers/action, store and combine reducer but I didn't see any datas in my page and i haven't any errors
There is my code :
Action
export const api = ext => `http://localhost:8080/${ext}`;
//
// ─── ACTION TYPES ───────────────────────────────────────────────────────────────
//
export const GET_ADVERTS = "GET_ADVERTS";
export const GET_ADVERT = "GET_ADVERT";
//
// ─── ACTION CREATORS ────────────────────────────────────────────────────────────
//
export function getAdverts() {
return dispatch => {
fetch("adverts")
.then(res => res.json())
.then(payload => {
dispatch({ type: GET_ADVERTS, payload });
});
};
}
export function getAdvert(id) {
return dispatch => {
fetch(`adverts/${id}`)
.then(res => res.json())
.then(payload => {
dispatch({ type: GET_ADVERT, payload });
});
};
}
reducer
import { combineReducers } from "redux";
import { GET_ADVERTS, GET_ADVERT } from "../actions/actions";
const INITIAL_STATE = {
adverts: [],
advert: {}
};
function todos(state = INITIAL_STATE, action) {
switch (action.type) {
case GET_ADVERTS:
return { ...state, adverts: action.payload };
case GET_ADVERT:
return { advert: action.payload };
default:
return state;
}
}
const todoApp = combineReducers({
todos
});
export default todoApp;
index.js
//imports
const store = createStore(todoApp, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("app")
);
My advertlist page :
//imports..
class Adverts extends Component {
componentDidMount() {
this.props.getAdverts();
}
render() {
const { adverts = [] } = this.props;
return (
<div>
<Header />
<h1>Adverts</h1>
{adverts.map(advert => (
<li key={advert._id}>
<a href={"adverts/" + advert._id}>
{advert.name} {advert.surname}
</a>
</li>
))}
<Footer />
</div>
);
}
}
const mapStateToProps = state => ({
adverts: state.adverts
});
export default connect(
mapStateToProps,
{ getAdverts }
)(Adverts);
I think your problem is here:
function mapStateToProps(state) {
return {
**adverts: state.adverts**
};
}
It should work if you change state.adverts to state.todos.adverts:
function mapStateToProps(state) {
return {
adverts: state.todos.adverts
};
}
Because your reducer is called todos, and it has state { adverts }, that's why you cannot access adverts even tho they are obtained.
You can check out working version here: https://codesandbox.io/s/olqxm4mkpq
The problem is, when you just create a store with one reducer without using combine reducer, it is possible to refer it directly in the ContainerS, like this:
const mapStateToProps = state => {
return{
*name of var*: state.adverts /*direct refers to adverts*/
}
}
But, when it use combined-reducer , it has to refer to an exact reducer that you want to use.like this :
const mapStateToProps = state => {
return{
*name of var* : state.todos.adverts (indirect refers to adverts from combined-reducer todos)
}
}
I am following the redux counter tutorial from the official docs,- but my applications state is seemingly not updating. To be more clear, essentially the application is a counter with an increment button and a decrement button and it displays the current value on the screen.
I can get it to console log the value as it changes, but it doesn't output it on the screen. Can anyone tell me what I'm doing wrong
import React, { Component } from 'react';
import { createStore } from 'redux';
const counter = (state = 0, action) => {
switch(action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state -1;
default:
return state;
}
}
const store = createStore(counter);
store.subscribe(()=>{
console.log(store.getState());
});
class App extends Component {
render() {
return (
<div className="App">
<h1>Counter Application</h1>
<hr/>
<Counter
value={store.getState()}
onIncrement={() => store.dispatch({type: 'INCREMENT'})}
onDecrement={() => store.dispatch({type: 'DECREMENT'})}
/>
</div>
);
}
}
const Counter = ({
value,
onIncrement,
onDecrement
}) => {
return(
<div>
<h1>{value}</h1>
<button onClick={onIncrement}> Plus </button>
<button onClick={onDecrement}> Minus </button>
</div>
)
}
export default App;
You're gonna need the provider and connect components from react-redux
Your App component won't re-render.
AFAIK, simply because a re-render can only be triggered if a component’s state or props has changed.
You need to trigger the your App component re-render inside store subscribe. I see your store subscribe basically do nothing, only logging here.
store.subscribe(()=>{
console.log(store.getState());
});
you could do something like this, to trigger re-render every time redux store updated:
const page = document.getElementById('page');
const render = () => ReactDOM.render(<App />, page);
render();
store.subscribe(render);
The reason:
In your case, the component has no idea about the changes in the redux store and therefore it doesn't re-render.
Components are only re-rendering if they receiv new props/context
or if their local state has updated (as a result of calling setState() in general)
Solution 1 (direct answer to your question, I think)
const Counter = ({ value, onIncrement, onDecrement }) => {
return (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}> Plus</button>
<button onClick={onDecrement}> Minus</button>
</div>
)
};
class App extends Component {
componentDidMount() {
this._unsub = store.subscribe(this._update);
this._update();
}
componentWillUnmount() {
this._unsub();
this._unsub = null;
};
state = { value: undefined };
render() {
const { value } = this.state;
return (
<div className="App">
<h1>Counter Application</h1>
<Counter
value={value}
onIncrement={this._increment}
onDecrement={this._decrement}
/>
</div>
);
}
_decrement = () => store.dispatch({ type: 'DECREMENT' });
_increment = () => store.dispatch({ type: 'INCREMENT' });
_update = () => {
const value = store.getState();
this.setState({ value });
}
}
Solution 2 (the correct one)
Use react-redux module
Also check these:
- normalizr
- normalizr + keyWindow concept talk
- Reselect
- ComputingDerivedData Post
- react-reselect-and-redux post
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { _valueSelector1, _valueSelector2 } from 'app/feature/selectors';
import { increment, decrement } from 'app/feature/actions';
const mapStateToProps = (state, props) => ({
valueOne: _valueSelector1(state, props),
valueTwo: _valueSelector2(state, props),
})
const mapDispatchToProps = {
increment,
decrement,
};
#connect(mapStateToProps, mapDispatchToProps)
export class YourComponent extends Component {
static propTypes = {
valueOne: PropTypes.number,
valueTwo: PropTypes.number,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
}
render() {
const { valueOne, valueTwo, increment, decrement } = this.props;
return (
<div>
<span>{valueOne}</span>
<Counter value={valueTwo} onIncrement={increment} onDecrement={decrement} />
</div>
)
}
}
I am trying to get a simple example to work. Here is the code below.
In this example, in:
mapStateToProps = (state) => {}
where is state coming from? I am little confused as to what exactly I am passing into?
I understand that connect(mapStateToProps)(TodoApp) "binds" the state returned in mapStateToProps to TodoApp and can then be accessed via this.props.
What do I need to do to this code so I can print out the current state inside TodoApp
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { connect } from 'react-redux'
import { createStore } from 'redux'
import { combineReducers } from 'redux'
const stateObject = [
{
'id': 1,
'name': 'eric'
},
{
'id': 2,
'name': 'john'
}
]
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text
}
default:
return state
}
}
const todos = (state = stateObject, action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
default:
return state
}
}
const store = createStore(todos)
//confused by what is happening here
const mapStateToProps = (state) => {
return {
?: ?
}
}
const TodoApp = () => {
//How do I get this to print out the current props?
console.log(this.props)
return (
<div>
Some Text
</div>
)
}
connect(mapStateToProps)(TodoApp)
ReactDOM.render(
<Provider store={store} >
<TodoApp />
</Provider>,
document.getElementById('root')
)
Ok updated:
const mapStateToProps = (state) => {
return {
names: state
}
}
const TodoApp = () => {
console.log(this.props)
return (
<div>
Some Text1
</div>
)
}
const ConnectedComponent = connect(mapStateToProps)(TodoApp);
ReactDOM.render(
<Provider store={store} >
<ConnectedComponent />
</Provider>,
document.getElementById('root')
)
However I'm still getting undefined for console.log(this.props).
What am I doing wrong?
There's no this with a functional component. To access the props you can change it to this:
const TodoApp = (props) => {
console.log(props)
return (
<div>
Some Text1
</div>
)
}
mapStateToProps maps the some parts of your Redux state to props of your React Component.
State comes from your store. In fact, you can take a look at your current state at any point by calling store.getState(). When you do createStore(todos), this creates the state based on the todos reducer. As you can see in your todos reducer, your initial state comes from stateObject, which is defined up top.
So, back to mapStateToProps. All you need to do in that functions is to return the object, where keys will be the props and values will be the values obtained from the Redux state. Here's an example of mapStateToProps:
const mapStateToProps = function (state) {
return {
propName: state
}
}
Now when you do the console.log(this.props) inside render(), you can see the whole state being stored inside this.props.propName. That is achieved by mapStateToProps.
A little bit of theory on this: each time an action is dispatched, every mapStateToProps you have in your app is called, props are applied to every component you created, and if any props have changed, that component will re-render. This kind of behaviour is provided for you via connect function. So you don't have to implement this behaviour for every component: all you need to do is to apply it like so: const ConnectedComponent = connect(mapStateToProps)(SomeComponent) and use ConnectedComponent instead of SomeComponent.