I am setting up a very basic react typescript and redux app.
I used useSelector() to retrieve state then use it in my component.
however when I dipatch to the store adding a new article the ui doesn't change, I checked redux dev tools and the store is updated, I read that useSelector automatically subscribes to store so I'm not sure why I'm having this problem.
my App component code:
function App() {
const dispatch: Dispatch<any> = useDispatch();
const articles: readonly IArticle[] = useSelector(
(state: ArticleState) => state.articles
);
const saveArticle = React.useCallback(
(article: IArticle) => dispatch(addArticle(article)),
[dispatch]
);
return (
<div className="App">
<header className="App-header">
<h1>My Articles</h1>
<AddArticle saveArticle={saveArticle} />
<ul>
{articles.map((article: IArticle) => (
<li>{article.title}</li>
))}
</ul>
</header>
</div>
);
}
export default App;
the addArticle ActionCreator
export function addArticle(article: IArticle) {
const action: ArticleAction = {
type: actionTypes.ADD_ARTICLE,
article,
};
return simulateHttpRequest(action);
}
The Reducer
const reducer = (
state: ArticleState = initialState,
action: ArticleAction
): ArticleState => {
switch (action.type) {
case actionTypes.ADD_ARTICLE:
const newState = {
...state,
};
const newArticle: IArticle = {
id: Math.random(), // not really unique
title: action.article.title,
body: action.article.body,
};
newState.articles.push(newArticle);
return newState;
case actionTypes.REMOVE_ARTICLE:
const newArticles = state.articles.filter(
(article) => article.id !== action.article.id
);
return {
...state,
articles: newArticles,
};
default:
return state;
}
};
export default reducer;
here's a screenshot I see that data is actually updating in the store
The line newState.articles.push(newArticle); is mutating the existing articles array. Your selector is then trying to read state.articles. Since it's the same reference as before, React-Redux assumes nothing has changed, and will not re-render:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
Please switch over to using Redux Toolkit for your store setup and reducer logic. Not only will it let you simplify your reducers by writing this kind of "mutating" logic and letting it create updates immutably, it effectively makes accidental mutations like this impossible.
Related
I had implemented useContext + useReducer and I found that I was getting re-renders when only dispatch changed.
I could have two separate components and if one dispatch was triggered both component got changed.
Example:
Both increment and decrement got rendered on each state update.
I found this article that I have followed but I still get the same result.
the code:
export default function App() {
return (
<MyContextProvider>
<Count />
<ButtonIncrement /> <br /> <br />
<ButtonDecrement />
</MyContextProvider>
);
}
Provider:
import * as React from 'react';
import {
InitalState,
ApplicationContextDispatch,
ApplicationContextState,
} from './Context';
import { applicationReducer } from './Reducer';
export const MyContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(applicationReducer, InitalState);
return (
<ApplicationContextDispatch.Provider value={{ dispatch }}>
<ApplicationContextState.Provider value={{ state }}>
{children}
</ApplicationContextState.Provider>
</ApplicationContextDispatch.Provider>
);
};
Context:
import React, { Dispatch } from 'react';
export enum ApplicationActions {
increment = 'increment',
decrement = 'decrement',
notification = 'Notification',
}
export type ActionType = ApplicationActions;
export const ActionTypes = { ...ApplicationActions };
export type StateType = {
count: number;
notification: string | undefined;
};
export type Action = {
type: ActionType;
payload?: string | undefined;
};
interface IContextPropsState {
state: StateType;
}
interface IContextPropsDispatch {
dispatch: Dispatch<Action>;
}
export const ApplicationContextState = React.createContext<IContextPropsState>(
{} as IContextPropsState
);
export const ApplicationContextDispatch =
React.createContext<IContextPropsDispatch>({} as IContextPropsDispatch);
export const useApplicationContextState = (): IContextPropsState => {
return React.useContext(ApplicationContextState);
};
export const useApplicationContextDispatch = (): IContextPropsDispatch => {
return React.useContext(ApplicationContextDispatch);
};
export const InitalState: StateType = {
count: 0,
notification: '',
};
Reducer:
import { StateType, Action, ActionTypes } from './Context';
export const applicationReducer = (
state: StateType,
action: Action
): StateType => {
const { type } = action;
switch (type) {
case ActionTypes.increment:
return { ...state, count: state.count + 1 };
case ActionTypes.decrement:
return { ...state, count: state.count - 1 };
case ActionTypes.notification:
return { ...state, notification: action.payload };
default:
return state;
}
};
Working example here
In the article above this fiddle was presented as an example which I based my attempt on but I dont know where Im going wrong.
Note that the original example of this was done without typescript but in my attempt I am adding typescript into the mix.
The problem is that you are passing a new object into your context providers. It's a classic gotcha. Passing objects as props means you are passing a new object every time which will fail prop reference checks.
Pass dispatch, state directly to the providers i.e. value={dispatch}
https://reactjs.org/docs/context.html#caveats
Caveats
Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:
<MyContext.Provider value={{something: 'something'}}>
#Yuji 'Tomita' Tomita Was 100% on the money.
I had to change my code so that it did not wrap the state and disatch inot an object which in turn made it work as desiered.
Updated code here: https://stackblitz.com/edit/react-ts-7nuhzk?file=Provider.tsx,ButtonDecrement.tsx,App.tsx
I learn about redux and its features. One thing that I get trouble with is createselector in reduxtoolkit. I have a slice:
const titlesSlice = createSlice({
name: "title",
initialState: {
titles: [],
title: "",
},
reducers: {
addTitle: (state, action) => {
state.titles.push({
id: Math.trunc(Math.random() * 10000).toString(),
title: state.title,
});
},
titleChange: (state, action) => {
state.title = action.payload;
},
},
});
and a selectors like:
const getTitles = (state) => (state.titles.titles);
export const selectTitlesLengthWithReselect = createSelector(
[getTitles],
(titles) => titles.filter(elem => elem.title.length > 5))
In App.js I added input for adding title:
function App(props) {
const dispatch = useDispatch();
const title = useSelector((state) => state.titles.title);
return (
<div className="App">
<div>
<input type="text"
onChange={(e) => dispatch(titleChange(e.target.value))}
value={title} />
<button onClick={() => dispatch(addTitle())}>Save</button>
<Titlelist />
</div>
</div>
);
}
TitleList component:
const Titlelist = () => {
const allTitles = useSelector(selectTitlesLengthWithReselect);
console.log("RENDERED");
return (
<div>
{allTitles.map((elem) => (
<li key={elem.id}>{elem.title}</li>
))}
</div>
)
}
Problem is every time input value(title in the titleReducer) changes the TitleList component rerenders. But the data that comes from selector is memoized(I checked for the prev and current value equality and they are same). Is there something that I'm doing wrong or why does component rerenders?
Nothing wrong with your selector, it is memoized, the selector did not cause the re render (re creating of jsx).
Dispatching titleChange will re render App so it will re render Titlelist because it is not a pure component, you can make it a pure component with React.memo:
const Titlelist = React.memo(function TitleList() {
const allTitles = useSelector(
selectTitlesLengthWithReselect
);
console.log('RENDERED');
return (
<div>
{allTitles.map((elem) => (
<li key={elem.id}>{elem.title}</li>
))}
</div>
);
});
As phry commented; it is usually fine to let your component re create jsx as React will do a virtual DOM compare and not re render DOM if the jsx is the same, if you have handlers that are re created (like: onClick={()=>new ref}) then that will fail virtual DOM compare. You can use useCallback to prevent that.
A pure component will generate the same jsx reference if no props changed so will have a quicker virtual dom compare but will also take up more memory and setup processing so it may not always benefit your application.
I'm writing a independent modal using React and Redux. I pass from my environment variable if modal is visible and initial position and the rest of the state in redux store.
I've tried using react lifecycle methods to force update my app but nothing seems to work.
This is how I connect my App with store:
render() {
const {
media, initPosition, isMobile, title, isVisible, onClose
} = this.props;
const photos = media.filter(
item => typeof item.video === 'undefined'
);
const videos = media.filter(
item => typeof item.video !== 'undefined'
);
const initState = {
media: {
items: media,
filteredItems: {
all: media,
photos,
videos
},
filter: 'all',
initPosition,
currentPosition: initPosition
},
gallery: {
isMobile,
title
}
};
const store = createStore(
reducer,
initState,
composeEnhancers(applyMiddleware(...middleware))
);
return (
<Provider store={store}>
<App onClose={e => onClose(e)} isVisible={isVisible} />
</Provider>
);
I call my modal like this:
<Gallery
media={videos.concat(photos)}
isMobile={isMobile}
isVisible={show}
onClose={() => this.setState({ show: false })}
initPosition={position}
changePosition={position => this.setState({ position })}
title="Maximus"/>
And this is how I connect it to the state:
function mapStateToProps(state) {
const { media, gallery } = state;
const {
filteredItems, filter, currentPosition, initPosition
} = media;
const { isMobile, title } = gallery;
return {
filteredMedia: filteredItems,
filter,
currentPosition,
initPosition,
isMobile,
title
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
changeMediaProp
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(
GalleryApp
);
After isVisible is changed nothing seem to work with redux store. It is changing, but the app isn't updating.
When I toggle modal (change isVisible prop), redux state keeps changing, but my app isn't rerendering.
So to sum it up. I change isVisible and initPosition from surrounded application( these props are not stored in store), and when I changed them my component can't react to changes from reducer store.
I was passing multiple stores to my application. I fixed it by saving store in constructor and not creating it multiple times.
let newStore = store;
if (!newStore) {
newStore = createStore(
reducer,
initState,
composeEnhancers(applyMiddleware(...middleware))
);
this.setState({ store: newStore });
}
return (
<Provider store={newStore}>
<App onClose={e => onClose(e)} isVisible={isVisible} />
</Provider>
);
Does anyone have any better solution?
Your code doesn't have enough information to know. Are you using connect and react-redux. Here's a good intro if you need some help.
https://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/
An example component would look like this:
import { connect } from 'react-redux'
const TodoItem = ({ todo, destroyTodo }) => {
return (
<div>
{todo.text}
<span onClick={destroyTodo}> x </span>
</div>
)
}
const mapStateToProps = state => {
return {
todo: state.todos[0]
}
}
const mapDispatchToProps = dispatch => {
return {
destroyTodo: () =>
dispatch({
type: 'DESTROY_TODO'
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TodoItem)
I am trying to display my state (users) in my react/redux functional component:
const Dumb = ({ users }) => {
console.log('users', users)
return (
<div>
<ul>
{users.map(user => <li>user</li>)}
</ul>
</div>
)
}
const data = state => ({
users: state
})
connect(data, null)(Dumb)
Dumb is used in a container component. The users.map statement has an issue but I thought that the data was injected through the connect statement? the reducer has an initial state with 1 name in it:
const users = (state = ['Jack'], action) => {
switch (action.type) {
case 'RECEIVED_DATA':
return action.data
default:
return state
}
}
CodeSandbox
You aren't using the connected component while rendering and hence the props aren't available in the component
const ConnectedDumb = connect(
data,
null
)(Dumb);
class Container extends React.Component {
render() {
return (
<div>
<ConnectedDumb />
</div>
);
}
}
Working demo
I have been using react+redux quite while, but could you any one help me the following case, on codepen:
const {createStore } = Redux;
const { Provider, connect } = ReactRedux;
const store = createStore((state={name: 'ron'}, action) => {
switch(action.type) {
case 'changeName': return {name: action.name};
default: return state
}
})
const Person = props => {
const {name, dispatch} = props
console.log(`rendering Person due to name changed to ${name}`)
return (
<div>
<p> My name is {name} </p>
<button onClick={ () => dispatch({type: 'changeName', name: 'ron'}) } > Change to Ron </button>
<button onClick={ () => dispatch({type: 'changeName', name: 'john'}) } > Change to John</button>
</div>
)
}
const App = connect(state=>state)(Person)
ReactDOM.render(
<Provider store={store}><App/></Provider>,
document.getElementById('root')
);
It is simple react app, but I cannot explain:
Initialise redux store with one reducer, and its initValue is {name: 'ron'}
Click Change to ron button, it will dispatch {type: 'changeName', name: 'ron'}
When the reducer get this action, it will generate an brand new state {name: 'ron'}, though the value is same as the original state, but they are different identity and should be the different ones.
The functional component should be re-rendered if the props changed even though the values are the same. So I suppose the render function will be called, and console should output rendering Person due to.... However, it is not happening.
I am wondering why react functional component refuse to render again when the props identity are changed (though the values are the same)
Your connect(state=>state)(Person) I think it's not wrong but it's weird.
According to the documentation https://redux.js.org/docs/basics/UsageWithReact.html you can separate the state and the action dispatcher, commonly naming mapStateToProps and mapDispatchToProps.
So, I propose to you this code:
const mapStateToProps = state => ({
user: state.user
})
const mapDispatchToProps = dispatch => ({
updateName: (name) => dispatch(changeName(name)),
})
class DemoContainer extends Component {
constructor() {
super();
}
render() {
return (
<div>
<p> My name is {this.props.user.name}</p>
<button onClick={ () => this.props.updateName('ron') } > Change to Ron </button>
<button onClick={ () => this.props.updateName('john') } > Change to John</button>
</div>
);
}
}
const Demo = connect(
mapStateToProps,
mapDispatchToProps
)(DemoContainer)
export default Demo
My reducer:
const initialState = { name: 'John'}
const user = (state = initialState, action) => {
switch (action.type) {
case "CHANGE_NAME":
return {
name: action.name
}
default:
return state
}
}
export default user
My action:
export const changeName = ( name ) => ({
type: "CHANGE_NAME",
name,
})
You can check all my code here: https://stackblitz.com/edit/react-tchqrg
I have a class for the component but you can also use a functional component with connect like you do.