简体   繁体   中英

React + Redux : reducer won't reload component

I wrote a generic Table component, instancied twice with two differents datasets. I use a Redux store to hydrate props and I try to delete a row when I click on it.

Components are well displayed and onClick function is fired and reducer is called. The new returned state returns the previous state without the element clicked.

My problem is that the components are not re-rendered. According to the troubleshouting page ( http://redux.js.org/docs/Troubleshooting.html ), it seems that I don't mutate the previous state (maybe I just don't understand how to do), I'm calling the dispatch() function and my mapStateToProps seems to be right as datas are well displayed on the page load.

I tried to define initial state in reducer.js > table function instead of doing it with createStore function, I tried to split 'tables' reducer into 'domains' and 'hosts' reducers (duplicate code), I tried to not use combineReducers() as I have only one (but I will have more in the future), and I tried so many things so I can't remember.

I bet it's not a big deal but I just not able to figure out what's going on. Can you help me please ? Thank you very much.


reducer.js

import { combineReducers } from 'redux'
import { DELETE_DOMAIN, DELETE_HOST } from './actions'

function deleteItem(state, index) {
    let newState = Object.assign({}, state)

    newState.items.splice(index, 1)

    return newState
}

function tables(state = {}, action) {
    switch (action.type) {
        case DELETE_HOST:
            return Object.assign({}, state, {
                hosts: deleteItem(state.hosts, action.host)
            })

        case DELETE_DOMAIN:
            return Object.assign({}, state, {
                domains: deleteItem(state.domains, action.domain)
            })

        default:
            return state
    }
}

const reducers = combineReducers({
    tables,
})

export default reducers

actions.js

export const DELETE_DOMAIN = 'DELETE_DOMAIN'
export const DELETE_HOST = 'DELETE_HOST'

export function deleteDomain(domain) {
    return { type: DELETE_DOMAIN, domain }
}

export function deleteHost(host) {
    return { type: DELETE_HOST, host }
}

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import reducers from './reducers'

const initialState = {
    tables: {
        domains: {
            headers: {
                id: 'Id',
                domain: 'Domain',
                host: 'Hoster',
            },
            items: [
                {
                    id: 1,
                    domain: 'dev.example.com',
                    host: 'dev',
                },
                {
                    id: 2,
                    domain: 'prod.example.com',
                    host: 'prod',
                }
            ]
        },
        hosts: {
            headers: {
                id: 'Id',
                label: 'Label',
                type: 'Type',
                hoster: 'Corporation',
            },
            items: [
                {
                    id: 1,
                    label: 'Server 1',
                    type: 'Server type',
                    hoster: 'Gandi',
                },
                {
                    id: 2,
                    label: 'Server 2',
                    type: 'Server type',
                    hoster: 'OVH',
                }
            ]
        }
    }
}

let store = createStore(reducers, initialState)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

app.js

import React from 'react';
import Domains from './modules/domains.js';
import Hosts from './modules/hosts.js';
import Nav from './modules/nav.js';
import {
  BrowserRouter as Router,
  Route,
} from 'react-router-dom'

export default class App extends React.Component {
    render() {
        return (
            <Router>
                <div className="container-fluid col-md-6">
                    <Nav />
                    <Route path="/domains" component={Domains}/>
                    <Route path="/hosts" component={Hosts}/>
                </div>
            </Router>
        );
    }
}

domain.js

import React from 'react';
import Table from './../components/table.js';
import { connect } from 'react-redux'
import { deleteDomain } from './../actions'

const mapStateToProps = (state) => {
    return state.tables.domains
}

const mapDispatchToProps = (dispatch) => {
    return {
            onRowClick: (id) => {
                dispatch(deleteDomain(id))
            }
        }
}

class DomainsView extends React.Component {
    render() {
        return (
            <Table headers={this.props.headers} items={this.props.items} onRowClick={this.props.onRowClick} />
        )
    }
}

const Domains = connect(
    mapStateToProps,
    mapDispatchToProps,
)(DomainsView)

export default Domains

hosts.js Same as domains.js with differents const / class names and props

Since your redux state is deeply nested, Object.assign does not make an actual duplicate. Although the state itself is a duplicate, its values are references to the same objects as before. As a result, your deeply nested objects are not duplicated. My advice is to use the merge method from lodash instead of Object.assign in your reducer. Install lodash via npm and then:

import { combineReducers } from 'redux'
import { merge } from 'lodash'
import { DELETE_DOMAIN, DELETE_HOST } from './actions'

function deleteItem(state, index) {
    let newState = merge({}, state);

    newState.items.splice(index, 1)

    return newState
}

function tables(state = {}, action) {
    let newState;
    switch (action.type) {
        case DELETE_HOST:
            newState = merge({}, state);
            newState.hosts = deleteItem(state.hosts, action.host)
            return newState;
        case DELETE_DOMAIN:
            newState = merge({}, state);
            newState.domains = deleteItem(state.domains, action.domain)
            return newState;
        default:
            return state
    }
}

const reducers = combineReducers({
    tables,
})

export default reducers

You should "flatten" your state. One of the reasons for this is immutably updating state can get verbose and hard to understand pretty quickly when it contains nested items. See this example in the Redux docs on this topic: http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html#correct-approach-copying-all-levels-of-nested-data

Check out this part of the Redux docs on producing a flatter state: http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html . You basically create a mini-relational database. In your example you would separate headers and items into their own arrays, each item in the array having a unique id. For domains and hosts you would have a key of header_ids and item_ids which contain an array of ids for the referenced data.

This simplifies adding and removing headers and items too because you just delete them by id from their own array and remove their id from the domains or hosts list of header or item ids.

Again, this part of the Redux docs will show you exactly how to do this with examples: http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html .

Side note: this is not my favorite aspect of Redux!

Thanks a lot guys. Using Lodash was the solution. However, I flattened my state as a good practice

Here are the result :

index.js

...
const initialState = {
    tables: {
        domains_headers: {
            id: 'Id',
            domain: 'Domain',
            host: 'Host',
        },
        domains_items: [
            {
                id: 1,
                domain: 'domain_1',
                host: 'host_domain_1',
            },
            {
                id: 2,
                domain: 'domain_2',
                host: 'host_domain_2',
            }
        ],
        hosts_headers: {
            id: 'Id',
            label: 'Label',
            type: 'Type',
            hoster: 'Corporation',
        },
        hosts_items: [
            {
                id: 1,
                label: 'label_host_1',
                type: 'type_host_1',
                hoster: 'hoster_host_1',
            },
            {
                id: 2,
                label: 'label_host_2',
                type: 'type_host_2',
                hoster: 'hoster_host_2',
            }
        ]
    }
}
...

reducer.js

import { combineReducers } from 'redux'
import { DELETE_DOMAIN, DELETE_HOST } from './actions'
import { merge } from 'lodash'

function deleteItem(items, index) {
    items.splice(index, 1)

    return items
}

function tables(state = {}, action) {
    let newState = merge({}, state)

    switch (action.type) {
        case DELETE_HOST:
            newState.hosts_items = deleteItem(newState.hosts_items, action.host)
            return newState

        case DELETE_DOMAIN:
            newState.domains_items = deleteItem(newState.domains_items, action.domain)
            return newState

        default:
            return state
    }
}

const reducers = combineReducers({
    tables,
})

export default reducers

domains.js

...
const mapStateToProps = (state) => {
    return {
        headers: state.tables.domains_headers,
        items: state.tables.domains_items
    }
}
...

hosts.js

...
const mapStateToProps = (state) => {
    return {
        headers: state.tables.hosts_headers,
        items: state.tables.hosts_items
    }
}
...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM