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.
Related
I've tried to search about for this issue but most of the topic is about mutate but it's a primitive datatype in my case and here is the example code:
// ParentPage.jsx
import { useSelector } from 'react-redux';
const selectShowFooterFlag = (state) => {
return state.shouldShowFooter;
}
const ParentPage = () => {
const shouldShowFooter = useSelector(selectShowFooterFlag);
const ChildComponent = useSelector(selectChildComponent);
return (
<p>{String(shoudShowFooter)}</p>
<ChildComponent shoudShowFooter={shoudShowFooter}>
)
}
export default ParentPage;
// ChildPage
const ChildPage = ({ shoudShowFooter }) => {
useEffect(() => {
dispatch(shouldShowFooterState(false));
return () => {
dispatch(shouldShowFooterState(true));
}
});
return (
<p>String(shoudShowFooter)</p>
)
}
// reducer
const initalState = {
isPageFooterRequired: true,
};
export const pagesReducer: (draft, action) = produce((draft = initialState, action: DispatchAction) => {
switch() {
case SHOW_FOOTER:
draft.shoudShowFooter = action.payload;
break;
default:
return draft;
}
});
With this example, when child component mounted, it will set shoudShowFooter to false as expected. When ChildComponent updated in redux store, the current ChildComponent will unmount and set the shoudShowFooter to true. I've checked that and it's correct as expected (the selectShowFooterFlag has been called).
But the issue is that shoudShowFooter changed in redux but ParentPage not re-render with new state so that new ChildComponent can't receive the latest data in reducer store (I've checked with the latest state data by Redux devtools).
One more interesting point is that when I switch to use connect, everything working just fine.
import { connect } from 'react-redux';
const selectShowFooterFlag = (state) => {
return state.shouldShowFooter;
}
const ParentPage = () => {
const ChildComponent = useSelector(selectChildComponent);
return (
<p>{String(shoudShowFooter)}</p>
<ChildComponent shoudShowFooter={shoudShowFooter}>
)
}
const mapStateToProps = (state) => {
return {
shouldShowFooter: selectShowFooterFlag(state);
}
}
export default connect(mapStateToProps)(ParentPage);
I think useSelector will give us the same behavior like connect, will re-render component when state data change. But seems like it's not in this case.
How could I make useSelector trigger re-render with latest data like what connect does? Or how could I debug this case to see why React not re-render the app?
Not able to access the redux store current state in a Class component.
It shows up console error
Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it.
When I tried to implement the same using a function component with useSelector and useDispatch, everything works as expected. What has gone wrong over here?
reducer.js
let initialState={
count:0
}
const reducer=(state=initialState,action)=>{
switch(action.type){
case ADD_INCREMENT:
return {
...state,
count:state.count+1
};
default: return state;
}
}
export default reducer;
action.js
const Increment=()=>{
return {
type:ADD_INCREMENT
}
}
store.js
import reducer from './reducer';
const store=createStore(reducer);
export default store;
Class Component
import { connect } from 'react-redux';
const mapStateToProps=state=>{
return {
count:state.count
}
}
const mapDispatchToProps=(dispatch)=>{
return {
count:()=>dispatch(action.Increment())
}
}
class Orders extends Component {
render() {
return (
<div>
<h1>Count: {this.props.count} </h1>
</div>
);
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Orders);
In App.js the entire container is wrapped with Provider and store is passed as props
Issue
You've named your state and your action both count, the latter is the one injected as a prop.
const mapStateToProps = state => {
return {
count: state.count // <-- name conflict
}
}
const mapDispatchToProps = (dispatch) => {
return {
count: () => dispatch(action.Increment()) // <-- name conflict
}
}
Solution
Provide different names, count for the state, maybe increment for the action.
const mapStateToProps = state => ({
count: state.count,
});
const mapDispatchToProps = (dispatch) => ({
increment: () => dispatch(action.Increment())
})
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.
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
In this little Redux app i'm trying to make two very simple counters which can operate independently from each other.
At the moment only Counter1 is connected to the store:
const App = connect(mapStateToProps, mapDispatchToProps)(Counter1);
And store is made up from allReducers:
const store = createStore(allReducers, composeEnchancers());
So, i'm getting this:
Uncaught Error: Objects are not valid as a React child (found: object with keys {CounterReducer1, CounterReducer2}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons. Check the render method of Counter1.
However if i make this:
const store = createStore(CounterReducer1, composeEnchancers());
Everything starts to work fine...
I know that in this case mistake might be very stupid, but i still can't figure out where exactly is it hiding...
Here is my entire code:
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore, bindActionCreators, compose, combineReducers} from 'redux';
import {Provider, connect} from 'react-redux';
//REDUCERS-----------------------------------------------------------------
function CounterReducer1(state=1, action){
switch(action.type){
case 'INCREASE1':
return state=state+1;
default:
return state;
}
}
function CounterReducer2(state=1, action){
switch(action.type){
case 'INCREASE2':
return state=state+10;
default:
return state;
}
}
const allReducers = combineReducers({
CounterReducer1,
CounterReducer2
});
//REDUCERS------------------------------------------------------------------
//COMPONENT COUNTER1--------------------------------------------------------
const Counter1 = ({ counter1, BtnClickHandler1 }) => {
return(
<div>
<button onClick={() => BtnClickHandler1()}>Counter1Button</button>
<h2>{counter1}</h2>
</div>
);
};
//COMPONENT COUNTER1--------------------------------------------------------
//COMPONENT COUNTER2--------------------------------------------------------
const Counter2 = ({ counter2, BtnClickHandler2 }) => {
return(
<div>
<button onClick={() => BtnClickHandler2()}>Counter2Button</button>
<h2>{counter2}</h2>
</div>
);
};
//COMPONENT COUNTER2-------------------------------------------
//ROOT COMPONENT-----------------------------------------------------------
const RootComponent = () => {
return (
<div>
<Counter1 />
<Counter2 />
</div>
);
};
//ROOT COMPONENT--------------------------------------------------------
//CONNECTING COUNTERS TO THE STORE-----------------------------
const mapStateToProps = (state) => {
return{
counter1: state,
counter2: state
};
};
const mapDispatchToProps = (dispatch) => {
return {
BtnClickHandler1: () => dispatch({type: 'INCREASE1'}),
BtnClickHandler2: () => dispatch({type: 'INCREASE2'})
};
};
const App = connect(mapStateToProps, mapDispatchToProps)(Counter1);
//CONNECTING COUNTERS TO THE STORE-----------------------------
//STORE-------------------------------------------------------------------
const composeEnchancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(allReducers, composeEnchancers());
//STORE-------------------------------------------------------------------
//RENDERING-----------------------------------------------------------------
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app'));
//RENDERING-----------------------------------------------------------------
First and very important concept is you should not mutate state. Update your reducers as:
function CounterReducer1(state=1, action){
switch(action.type){
case 'INCREASE1':
return state+1;
default:
return state;
}
}
function CounterReducer2(state=1, action){
switch(action.type){
case 'INCREASE2':
return state+10;
default:
return state;
}
}
You have only Counter1 component connected. You can create just one container component that's connected and render multiple presentational components(Counter1 and Counter2). Move you mappings(mapStateToProps/mapDispatchToProps) and connect code in container component and you can pass the counter results as props to this presentational components.
When you combine reducers, if you dont provide any key, the reducer names which you provide are taken as keys. In that case, you have to get the props as below:
return {
counter1: state.CounterReducer1,
counter2: state.CounterReducer2
};
You're trying to pass the whole state (the result of combineReducers()) to each component, which will result in having an object as props. This is fine, of course, but it's not what you want in this case. I'm talking about the following part of the code:
return {
counter1: state,
counter2: state
};
If you'd do a console.log(state) right before returning, you would see this:
{CounterReducer1: 1, CounterReducer2: 1}
So in order to fix this, just return the appropriate part of the store to each component:
return {
counter1: state.CounterReducer1,
counter2: state.CounterReducer2
};