I m actually studying react + redux (https://github.com/davezuko/react-redux-starter-kit) and I m having some trouble with view rerendering.
I have this action / reducer :
/* Action part */
export const ADD_TODO_ITEM = 'ADD_TODO_ITEM';
export function addTodoItem(value = null): Action {
return {
type: ADD_TODO_ITEM,
payload: value
}
}
export const actions = {
addTodoItem
}
/* Reducer part */
const actionHandler = {
[ADD_TODO_ITEM]: (state, action) => {
state.push('New todo value')
console.log(state);
return state;
}
}
const initialState = [];
export default function todoReducer(state = initialState, action) {
const reducer = actionHandler[action.type];
return reducer ? reducer(state, action) : state;
}
It's a simple action / reducer that should add a new item inside of a todo list.
The todo list component looks like following :
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import TodoInput from './../TodoInput/TodoInput';
type Props = {
todos:Array
}
export class TodoList extends React.Component <void, Props, void>{
static propTypes = {
todos: PropTypes.array.isRequired
};
render(){
let i = 0;
return(
<div>
<h4>Paf</h4>
<TodoInput/>
<ul>
{
this.props.todos.map((item)=>{
i++;
return <li key={i}>{item}</li>
})
}
</ul>
</div>
)
}
}
const mapStateToProps = (state) =>({
todos: state.todos
});
export default connect((mapStateToProps), {
})(TodoList)
As you can see, I have a TodoInput component that only sends an action event when a button is pressed. This part works well, I can log it inside of the action / reducer file.
The fact is that my TodoList component is never rerendered, and my item are not displayed as expected.
It's more weird that in fact the list appears on the screen when I use the livereload tool to code (modifying a line in my editor makes a simple refresh on browser, and at this moment the state.todos is well rendered on the screen)
It seems that I made something bad, and I can't put a finger on it.
Can you help me ?
EDIT : Problem solved. In fact, I was using a mutating state, and it seems that redux can't detect the change on this. refers to https://github.com/reactjs/redux/issues/585
Related
I am new to using redux for React Native and am testing it with a simple case. I have been able to successfully connect to the store, and I can see the action is dispatched properly using the redux debugger, however, the store is not updating in the debugger. I've tried several different implementations, but nothing is working. Any help would be appreciated!
Component:
import React, { PureComponent } from 'react'
import { Text, TouchableOpacity, SafeAreaView, Alert, Button } from 'react-native'
import { Navigation } from 'react-native-navigation';
import { connect } from 'react-redux'
import simpleAction from '../store/actions/simpleAction'
class App2 extends PureComponent {
constructor(props){
super(props);
}
pressRedux = () => {
const data = 'hello'
this.props.simpleAction(data)
}
render() {
return (
<SafeAreaView>
<Text>
{this.props.state.simpleReducer.text}
</Text>
<Button onPress = {this.pressRedux} title = 'Redux' />
</SafeAreaView>
)
}
}
function mapStateToProps(state) {
return {
state: state
};
}
const mapDispatchToProps = {
simpleAction
}
export default connect(mapStateToProps, mapDispatchToProps)(App2);
Action:
import {SET_TEXT} from '../types/types'
export default function simpleAction(data) {
return({
type: SET_TEXT,
payload: data
})
}
reducer:
import SET_TEXT from '../types/types'
const INITIAL_STATE = {
text: 'Hi'
}
const simpleReducer = (state = INITIAL_STATE, action ) => {
switch(action.type){
case SET_TEXT:
return { ...state, text: action.payload};
default:
return state;
}
}
export default simpleReducer;
The code you've shared here looks correct. Only thing I can suggest is, if you're seeing the action come through in the debugger, your issue is either with the data/payload or logic within simpleReducer.
In this case you have it properly stripped down so I'd almost think this isn't actually the code you are running, it might be something in your build process?
I use React context with hooks as a state manager for my React app. Every time the value changes in the store, all the components re-render.
Is there any way to prevent React component to re-render?
Store config:
import React, { useReducer } from "react";
import rootReducer from "./reducers/rootReducer";
export const ApiContext = React.createContext();
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, {});
return (
<ApiContext.Provider value={{ ...state, dispatch }}>
{children}
</ApiContext.Provider>
);
};
An example of a reducer:
import * as types from "./../actionTypes";
const initialState = {
fetchedBooks: null
};
const bookReducer = (state = initialState, action) => {
switch (action.type) {
case types.GET_BOOKS:
return { ...state, fetchedBooks: action.payload };
default:
return state;
}
};
export default bookReducer;
Root reducer, that can combine as many reducers, as possible:
import userReducer from "./userReducer";
import bookReducer from "./bookReducer";
const rootReducer = ({ users, books }, action) => ({
users: userReducer(users, action),
books: bookReducer(books, action)
});
An example of an action:
import * as types from "../actionTypes";
export const getBooks = async dispatch => {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
method: "GET"
});
const payload = await response.json();
dispatch({
type: types.GET_BOOKS,
payload
});
};
export default rootReducer;
And here's the book component:
import React, { useContext, useEffect } from "react";
import { ApiContext } from "../../store/StoreProvider";
import { getBooks } from "../../store/actions/bookActions";
const Books = () => {
const { dispatch, books } = useContext(ApiContext);
const contextValue = useContext(ApiContext);
useEffect(() => {
setTimeout(() => {
getBooks(dispatch);
}, 1000);
}, [dispatch]);
console.log(contextValue);
return (
<ApiContext.Consumer>
{value =>
value.books ? (
<div>
{value.books &&
value.books.fetchedBooks &&
value.books.fetchedBooks.title}
</div>
) : (
<div>Loading...</div>
)
}
</ApiContext.Consumer>
);
};
export default Books;
When the value changes in Books component, another my component Users re-renders:
import React, { useContext, useEffect } from "react";
import { ApiContext } from "../../store/StoreProvider";
import { getUsers } from "../../store/actions/userActions";
const Users = () => {
const { dispatch, users } = useContext(ApiContext);
const contextValue = useContext(ApiContext);
useEffect(() => {
getUsers(true, dispatch);
}, [dispatch]);
console.log(contextValue, "Value from store");
return <div>Users</div>;
};
export default Users;
What's the best way to optimize context re-renders? Thanks in advance!
Books and Users currently re-render on every cycle - not only in case of store value changes.
1. Prop and state changes
React re-renders the whole sub component tree starting with the component as root, where a change in props or state has happened. You change parent state by getUsers, so Books and Users re-render.
const App = () => {
const [state, dispatch] = React.useReducer(
state => ({
count: state.count + 1
}),
{ count: 0 }
);
return (
<div>
<Child />
<button onClick={dispatch}>Increment</button>
<p>
Click the button! Child will be re-rendered on every state change, while
not receiving any props (see console.log).
</p>
</div>
);
}
const Child = () => {
console.log("render Child");
return "Hello Child ";
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
Optimization technique
Use React.memo to prevent a re-render of a comp, if its own props haven't actually changed.
// prevents Child re-render, when the button in above snippet is clicked
const Child = React.memo(() => {
return "Hello Child ";
});
// equivalent to `PureComponent` or custom `shouldComponentUpdate` of class comps
Important: React.memo only checks prop changes (useContext value changes trigger re-render)!
2. Context changes
All context consumers (useContext) are automatically re-rendered, when the context value changes.
// here object reference is always a new object literal = re-render every cycle
<ApiContext.Provider value={{ ...state, dispatch }}>
{children}
</ApiContext.Provider>
Optimization technique
Make sure to have stable object references for the context value, e.g. by useMemo Hook.
const [state, dispatch] = useReducer(rootReducer, {});
const store = React.useMemo(() => ({ state, dispatch }), [state])
<ApiContext.Provider value={store}>
{children}
</ApiContext.Provider>
Other
Not sure, why you put all these constructs together in Books, just use one useContext:
const { dispatch, books } = useContext(ApiContext);
// drop these
const contextValue = useContext(ApiContext);
<ApiContext.Consumer> /* ... */ </ApiContext.Consumer>;
You also can have a look at this code example using both React.memo and useContext.
I believe what is happening here is expected behavior. The reason it renders twice is because you are automatically grabbing a new book/user when you visit the book or user page respectively.
This happens because the page loads, then useEffect kicks off and grabs a book or user, then the page needs to re-render in order to put the newly grabbed book or user into the DOM.
I have modified your CodePen in order to show that this is the case.. If you disable 'autoload' on the book or user page (I added a button for this), then browse off that page, then browse back to that page, you will see it only renders once.
I have also added a button which allows you to grab a new book or user on demand... this is to show how only the page which you are on gets re-rendered.
All in all, this is expected behavior, to my knowledge.
I tried to explain with different example hope that will help.
Because context uses reference identity to determine when to re-render, that could trigger unintentional renders in consumers when a provider’s parent re-renders.
for example: code below will re-render all consumers every time the Provider re-renders because a new object is always created for value
class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
To get around this, lift the value into the parent’s state
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
This solution is used to prevent a component from rendering in React is called shouldComponentUpdate. It is a lifecycle method which is available on React class components. Instead of having Square as a functional stateless component as before:
const Square = ({ number }) => <Item>{number * number}</Item>;
You can use a class component with a componentShouldUpdate method:
class Square extends Component {
shouldComponentUpdate(nextProps, nextState) {
...
}
render() {
return <Item>{this.props.number * this.props.number}</Item>;
}
}
As you can see, the shouldComponentUpdate class method has access to the next props and state before running the re-rendering of a component. That’s where you can decide to prevent the re-render by returning false from this method. If you return true, the component re-renders.
class Square extends Component {
shouldComponentUpdate(nextProps, nextState) {
if (this.props.number === nextProps.number) {
return false;
} else {
return true;
}
}
render() {
return <Item>{this.props.number * this.props.number}</Item>;
}
}
In this case, if the incoming number prop didn’t change, the component should not update. Try it yourself by adding console logs again to your components. The Square component shouldn’t rerender when the perspective changes. That’s a huge performance boost for your React application because all your child components don’t rerender with every rerender of their parent component. Finally, it’s up to you to prevent a rerender of a component.
Understanding this componentShouldUpdate method will surely help you out!
Our project is mainly written in AngularJS 1.5, but we are transitioning over to ReactJS. We're using react2angular to allow our AngularJS project to consume React components. Our project is also using ngRedux to store certain data. While I'm able to save certain data using Angular, I am unable to access the Redux store data via the mapped props.
this.props.interval in CardController.js is undefined. I am able to get store data using this.props.store.getState(), but React is not able to detect if there has been a change in the Redux store in componentWillReceiveProps, so I don't think that's an option.
CardContainer.js
import { connect } from 'react-redux';
import CardController from './CardController';
const mapStateToProps = (state) => {
return {
interval: state.interval
};
};
const mapDispatchToProps = null;
const ScorecardContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(CardController);
export default CardContainer;
CardController.js
import React from 'react';
import PropTypes from 'prop-types';
import Card from './Card';
export default class CardController extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: this.props.interval,
state: this.props.$scope.filterParams,
};
}
render() {
return (
<div className="CardController">
<Card
selected={this.state.selected}
/>
</div>
);
}
}
CardController.propTypes = {
interval: PropTypes.string,
};
reducers.js
import * as actionTypes from './actionTypes';
const initialState = {
interval : 'week'
};
export const setInterval = (state, action) => {
return {
...state,
interval : action.interval
};
};
export const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.SET_INTERVAL :
return setInterval(state, action);
default :
return state;
}
};
export default reducer;
Turns out that the problem was that I forgot to drill down. Instead of state.interval, it should have been state.card.interval.
I have a component TreeNav whose data comes from api call. I have setup reducer/action/promise and all the plumbing, but in component render when I call map() over the data, getting "Uncaught TypeError: Cannot read property 'map' of undefined".
Troubleshooting revealed TreeNav render() is called twice. 2nd time is after data comes back from api. But due to 1st render() error, 2nd render() never runs.
Here are my code files:
-------- reducers/index.js ---------
import { combineReducers } from 'redux';
import TreeDataReducer from './reducer_treedata';
const rootReducer = combineReducers({
treedata: TreeDataReducer
});
export default rootReducer;
-------- reducers/reducer_treedata.js ---------
import {FETCH_TREE_DATA} from '../actions/index';
export default function (state=[], action) {
switch (action.type) {
case FETCH_TREE_DATA: {
return [action.payload.data, ...state];
}
}
return state;
}
-------- actions/index.js --------
import axios from 'axios';
const ROOT_URL = 'http://localhost:8080/api';
export const FETCH_TREE_DATA = 'FETCH_TREE_DATA';
export function fetchTreeData () {
const url = `${ROOT_URL}/treedata`;
const request = axios.get(url);
return {
type: FETCH_TREE_DATA,
payload: request
};
}
-------- components/tree_nav.js --------
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {fetchTreeData} from '../actions/index';
class TreeNav extends Component {
constructor (props) {
super(props);
this.state = {treedata: null};
this.getTreeData();
}
getTreeData () {
this.props.fetchTreeData();
}
renderTreeData (treeNodeData) {
const text = treeNodeData.text;
return (
<div>
{text}
</div>
);
}
render () {
return (
<div className="tree-nav">
{this.props.treedata.children.map(this.renderTreeData)}
</div>
);
}
}
function mapStateToProps ({treedata}) {
return {treedata};
}
// anything returned from this function will end up as props
// on the tree nav
function mapDispatchToProps (dispatch) {
// whenever selectBook is called the result should be passed to all our reducers
return bindActionCreators({fetchTreeData}, dispatch);
}
// Promote tree_nav from a component to a container. Needs to know about
// this new dispatch method, fetchTreeData. Make it available as a prop.
export default connect(mapStateToProps, mapDispatchToProps)(TreeNav);
In terms of the error with your second render, the state must be getting overridden in a way you're not expecting. So in your reducer, you're returning array that contains whatever data is, and a splat of the current state. With arrays that does a concat.
var a = [1,2,3]
var b = [a, ...[2,3,4]]
Compiles to:
var a = [1, 2, 3];
var b = [a].concat([2, 3, 4]);
So given you're expecting a children property, what i think you actually want is a reducer that returns an object, not an array, and do something like this instead:
return Object.assign({}, state, { children: action.payload.data });
From there be sure to update the initial state to be an object, with an empty children array.
Get rid of this line since you're using props instead of state. State can be helpful if you need to manage changes just internally to the component. But you're leveraging connect, so that's not needed here.
this.state = {treedata: null};
Solved this by checking for the presence of this.props.treedata, etc. and if not available yet, just should div "loading...".
render () {
if (this.props.treedata && this.props.treedata[0] && this.props.treedata[0].children) {
console.dir(this.props.treedata[0].children);
return (
<div className="tree-nav">
{this.props.treedata[0].children.map(this.renderTreeData)}
</div>
);
} else {
return <div>Loading...</div>;
}
}
I m learning redux and react and I'm having a little problem concerning good practices of props initialization.
In fact, I m having a route that looks like following :
/pokemons/:name
And here's the concerned component :
import React from 'react';
import {connect} from 'react-redux';
import {showDetail} from './../../redux/modules/pokemons';
export class PokeDetail extends React.Component{
render(){
return(
<div>
{this.currentPokemon.name}
</div>
)
}
}
const mapStateToProps = state => ({
currentPokemon:state.pokemons.currentPokemon
});
export default connect((mapStateToProps),{
showDetail
})(PokeDetail);
The fact is that I don't know at all when / where to send my action to change my app state. In fact, when should I send my "showDetail('myPokemonName')" so that the currentPokemon state would change and my app work ?
I m needing some good practices if possible
Thanks for your help
EDIT :
My PokeView :
import React from 'react';
import {connect} from 'react-redux';
import {loadRemoteAction} from './../../redux/modules/pokemons';
import {Link} from 'react-router';
export class PokeView extends React.Component {
render(){
let i = 0;
return (
<div>
<h4>Super page</h4>
<button onClick={this.props.loadRemoteAction}>Start</button>
<ul>
{
this.props.pokemons.map(item => {
i++;
return <li key={i}><Link to={`/pokemons/${item.name}`}>{item.name}</Link></li>
})
}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
pokemons : state.pokemons.pokemons
});
export default connect((mapStateToProps), {
loadRemoteAction
})(PokeView);
My action / reducer :
import immutable from 'immutable';
/**
* Action part
*/
export const LOAD_REMOTE = 'LOAD_REMOTE';
export const SHOW_DETAIL = 'SHOW_DETAIL';
export function loadRemoteAction() {
return dispatch => {
fetch('http://pokeapi.co/api/v2/pokemon/')
.then(res => res.json())
.then(res => dispatch({
type:LOAD_REMOTE,
payload:res.results
}));
};
}
export function showDetail(name){
return {
type:SHOW_DETAIL,
payload:name
}
}
/**
* Reducer part
*/
const ACTION_HANDLER = {
[LOAD_REMOTE] : (state, action) => {
if(action.payload) return Object.assign({},state,{pokemons:state.pokemons.concat(action.payload)});
return state;
},
[SHOW_DETAIL] : (state, action) =>{
let currentPokemon;
for(const pkm of state.pokemons){
if(pkm.name === action.payload) currentPokemon = pkm;
}
if(action.payload) return Object.assign({},state,{currentPokemon:currentPokemon});
return state
}
}
const initialState = {pokemons:immutable.fromJS([]), currentPokemon:immutable.fromJS({name:''})};
export default function pokemonReducer (state = initialState, action) {
const handler = ACTION_HANDLER[action.type]
return handler ? handler(state, action) : state
}
Generally speaking, an action is when something happens in the world - this would usually either be something a user does, or e.g. an asynchronous answer from the backend to an ajax call. These are the situations in which you would want to send an action (containing the information about what was changed) so that your state tree can be updated accordingly.
In your case, if you show a list of Pokemons somewhere else on the screen, and the user clicks on one of them, then that click would save the clicked-on Pokemon to the state tree, and your PokeDetail component would then pick up this information and display the details for the selected Pokemon.
In your case, the PokeView render function might look like this:
export class PokeView extends React.Component {
render(){
return (
<div>
<h4>Super page</h4>
<button onClick={this.props.loadRemoteAction}>Start</button>
<ul>
{
this.props.pokemons.map((item, i) => <li key={i}><button onClick={this.props.dispatch(showDetail(item.name))}>{item.name}</button></li> )
}
</ul>
</div>
);
}
}
and the PokeDetail class might look like this:
export class PokeDetail extends React.Component{
render(){
return <div>{this.props.currentPokemon.name}</div>;
}
}
The other question is, how does the information go into my app initially? If the data is static, you can add it to your initial state tree (one usually passes that to the reducer function as default parameter), or you could query the data from the backend via an Ajax call. The latter can be done in the componentDidMount lifecycle method of your component. (For this, you need redux-thunk in order to have an action that works together with the callback and the asynchronous answer from the backend).
You should call showDetail action in componentWillMount function.
import React from 'react';
import {connect} from 'react-redux';
import {showDetail} from './../../redux/modules/pokemons';
export class PokeDetail extends React.Component{
componentWillMount() {
// If you are using react-router, route params will be in `params` property object.
const pokemonId = this.props.params.id;
this.props.showDetail(pokemonId);
}
componentWillReceiveProps(nextProps) {
// Also you may want to update your component when url was changed.
if (nextProps.params.id !== this.props.params.id) {
this.props.showDetail(nextProps.params.id);
}
}
render(){
return(
<div>
{this.currentPokemon.name}
</div>
)
}
}
const mapStateToProps = state => ({
currentPokemon:state.pokemons.currentPokemon
});
export default connect((mapStateToProps),{
showDetail
})(PokeDetail);
When your component is mounting(or updating), you are calling your action with pokemon id. Next your store will be updated and you will receive needed props in PokeDetail component.
Also for async actions you may need redux-thunk package.
You can use redux-async-connect package. You can find sample usage of this package here.