简体   繁体   中英

reactJs redux: how to dispatch an action on a user event and link actions with the reducers and the store

Being pretty new to reactJs & redux and despite the tutorials (including the todolist example from redux), I still have difficulties understanding how to actually trigger an action which will change the state.

I have already built something quite simple which is loading fine. Can somebody help me dispatch an action and that ends up in the store data being changed ?

I'd like the togglePriceModule function to be called when the user clicks on li.module . Do I need to call a function of the main Pricing component passed as a prop to the child? What is the right way to do it?

Thanks a lot !

my app.js :

//Importing with braces imports a specific export of the file
import { createDevTools } from 'redux-devtools'
//Importing without braces imports the default export of the file
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'

import React from 'react'
import ReactDOM from 'react-dom'
//Redux helps manage a single state which can be updated through actions call pure reducers
//Importing muliple items injects them into the current scope
import { applyMiddleware, compose, createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'
//React-router helps switch between components given a specific route
import { Router, Route, Link } from 'react-router'
import createHistory from 'history/lib/createHashHistory'
import { syncHistory, routeReducer } from 'react-router-redux'

//Imports an object of elements correspondings to every export of the file
import * as reducers from './reducers';

import Pricing from './components/pricing/pricing_main.js';

const history = createHistory();
const middleware = syncHistory(history);
const reducer = combineReducers({
    ...reducers,
    routing: routeReducer
});

const DevTools = createDevTools(
    <DockMonitor toggleVisibilityKey="ctrl-q"
                 changePositionKey="ctrl-alt-q"
                 defaultIsVisible={false}>
        <LogMonitor theme="tomorrow" preserveScrollTop={false} />
    </DockMonitor>
);

const finalCreateStore = compose(
    applyMiddleware(middleware),
    DevTools.instrument()
)(createStore);

const store = finalCreateStore(reducer);

middleware.listenForReplays(store);

var renderComponent = function(component, id) {
    var reactContainer = document.getElementById(id);
    if (null !== reactContainer) {
        ReactDOM.render(
            <Provider store={store}>
                <div>
                    <Router history={history}>
                        <Route path="/" component={component} />
                    </Router>
                    <DevTools />
                </div>
            </Provider>,
            reactContainer
        );
    }
};

renderComponent(Pricing, 'react-pricing');

My pricing components :

import React from 'react';
var _ = require('lodash');

const Pricing = React.createClass({
    getInitialState: function(){
        return {
            modules: {
                'cms' : {
                    title: 'Fiches techniques & Mercuriale',
                    subtitle: 'Gérez votre connaissance sous un format structuré',
                    price: 15,
                    details: [
                        'première ligne',
                        'deuxième ligne',
                        'troisième ligne'
                    ],
                    'activated': true
                },
                'cycle' : {
                    title: 'Cycle de menus',
                    subtitle: 'Programmez votre production dans le temps',
                    price: 20,
                    details: [
                        'première ligne',
                        'deuxième ligne',
                        'troisième ligne'
                    ],
                    'activated': false
                },
                'organigram' : {
                    title: 'Organigramme de production',
                    subtitle: "Optimisez l'affectation de votre main d'oeuvre",
                    price: 20,
                    details: [
                        'première ligne',
                        'deuxième ligne',
                        'troisième ligne'
                    ],
                    'activated': false
                },
                'teams' : {
                    title: 'Planning des équipes',
                    subtitle: "Gérez les temps de présence de vos salariés",
                    price: 20,
                    details: [
                        'première ligne',
                        'deuxième ligne',
                        'troisième ligne'
                    ],
                    'activated': false
                },
                'orders' : {
                    title: 'Commandes et stocks',
                    subtitle: "Commandez en un clic auprès de vos fournisseurs",
                    price: 20,
                    details: [
                        'première ligne',
                        'deuxième ligne',
                        'troisième ligne'
                    ],
                    'activated': false
                }
            },
            options : {
                users: {
                    title: "Nombre d'utilisateurs",
                    subtitle: "Distribuez des accès sécurisés",
                    price: 5,
                    unit: "5€ par utilisateur",
                    type: 'quantity',
                    value: 1
                },
                sites: {
                    title: 'Sites de vente ou de production',
                    subtitle: "Gérez vos multiples sites dans la même interface",
                    unit: "50€ par site",
                    type: 'quantity',
                    value: 1
                },
                backup: {
                    title: 'Sauvegarde',
                    subtitle: "Recevez une copie Excel de vos données tous les jours",
                    type: 'switch',
                    value: 'day'
                }
            }
        }
    },
    componentWillMount: function(){
        this.setState(this.getInitialState());
    },
    render: function () {
        return (
            <div className="wrapper">
                <h1>Paramétrez votre offre</h1>
                <div id="elements">
                    <ul id="module-container" className="flex-container col">
                        {_.map(this.state.modules, function(module, key) {
                            return <Module key={key} data={module} />
                        })}
                    </ul>
                    <ul id="param_container">
                        {_.map(this.state.options, function(option, key) {
                            return <Option key={key} data={option} />
                        })}
                    </ul>
                </div>
                <div id="totals" className="flex-container sp-bt">
                    <span>Total</span>
                    <span>{calculatePrice(this.state)}</span>
                </div>
            </div>
        );
    }
});

function calculatePrice(state) {
    var modulePrices = _.map(state.modules, function(item){
        return item.price;
    });
    modulePrices = _.sum(modulePrices);

    return modulePrices;
}

var Module = React.createClass({
    render: function(){
        var data = this.props.data;
        return <li className="module">
            <div className="selection">
                <i className={data.activated ? 'fa fa-check-square-o' : 'fa fa-square-o'} />
            </div>
            <div className="title">
                <h3>{data.title}</h3>
                <h4>{data.subtitle}</h4>
            </div>
            <div className="price">
                <div className="figure">{data.price}</div>
                <div className="period">par mois</div>
            </div>
            <ul className="details">{
                data.details.map(function(item, key){
                    return <li key={key}><i className="fa fa-check" />{item}</li>
                })}
            </ul>
        </li>
    }
});

var Option = React.createClass({
    render: function(){
        var data = this.props.data;
        return <li className="param">
            <div className="title">
                <h3>{data.title}</h3>
                <h4>{data.subtitle}</h4>
            </div>
            <div className="config">
                <span className="figure"><i className="fa fa-minus" /></span>
                <input value="1"/>
                <span className="plus"><i className="fa fa-plus" /></span>
            </div>
        </li>
    }
});

export default Pricing;

my actions :

import { TOGGLE_PRICE_MODULE, INCREASE_PRICE_OPTION, DECREASE_PRICE_OPTION } from '../constants/constants.js'

export function increasePriceOption(value) {
    return {
        type: INCREASE_PRICE_OPTION,
        value: value
    }
}

export function decreasePriceOption(value) {
    return {
        type: DECREASE_PRICE_OPTION,
        value: value
    }
}

export function togglePriceModule(activated) {
    return {
        type: TOGGLE_PRICE_MODULE,
        activated: activated
    }
}

my reducers:

import { TOGGLE_PRICE_MODULE, INCREASE_PRICE_OPTION, DECREASE_PRICE_OPTION } from '../constants/constants.js'


export default function updateModule(state = false, action) {
    if(action.type === TOGGLE_PRICE_MODULE) {
        return !state;
    }
    return state
}

export default function updateOption(state = 1, action) {
    if(action.type === INCREASE_PRICE_OPTION) {
        return state + 1;
    }
    else if(action.type === DECREASE_PRICE_OPTION) {
        if (state < 2) {
            return 1;
        } else {
            return state + 1;
        }
    }
    return state

EDIT 1

I've isolated the Module component and tried to adapt from the first answer below : the modules loads properly but there is no effect at all in the view. Missing something ?

first error : I needed to change

import * as reducers from './reducers';

into

import * as reducers from './reducers/pricing.js';

for a console log to actually show my reducers.

Why?

Second: A console.log shows that the action is indeed being called The same in the reducer shows it's not. How should I make the link between the reducer and the action ? Should I use mapStateToProps and connect somehow ?

import React from 'react';
import { togglePriceModule } from '../../actions/pricing.js';

var Module = React.createClass({
    handleClick: function(status){
        this.context.store.dispatch(togglePriceModule(status));
    },
    render: function(){
        console.log(this.props);
        var data = this.props.data;
        return <li className="module" onClick={this.handleClick}>
            <div className="selection">
                <i className={data.activated ? 'fa fa-check-square-o' : 'fa fa-square-o'} />
            </div>
            <div className="title">
                <h3>{data.title}</h3>
                <h4>{data.subtitle}</h4>
            </div>
            <div className="price">
                <div className="figure">{data.price}</div>
                <div className="period">par mois</div>
            </div>
            <ul className="details">{
                data.details.map(function(item, key){
                    return <li key={key}><i className="fa fa-check" />{item}</li>
                })}
            </ul>
        </li>
    }
});

Module.contextTypes = {
    store: React.PropTypes.object
};

export default Module;
    }

EDIT2

I've made changes as suggested and the reducer is now called. However I have no UI change so I'm gessing I did something wrong. Is the way I am handling the state / the store / the prop right ?

The bundle is valid but I get the following error in the console :

warning.js:45 Warning: setState(...): Cannot update during an existing state transition (such as within render ). Render methods should be a pure function of props and state.

Also, should I pass a function from the pricing component (a container) to the module component and put the logic above instead of dispatching the action in the child module component ?

My updated module component on which I click in the hope of a UI change :

import React from 'react';
import { connect } from 'react-redux';
import { togglePriceModule } from '../../actions/pricing.js';

var Module = React.createClass({
    handleClick: function(status){
        this.context.store.dispatch(togglePriceModule(status));
    },
    render: function(){
        var data = this.props.data;
        return <li className="module" onClick={this.handleClick(!data.activated)}>
            <div className="selection">
                <i className={data.activated ? 'fa fa-check-square-o' : 'fa fa-square-o'} />
            </div>
            <div className="title">
                <h3>{data.title}</h3>
                <h4>{data.subtitle}</h4>
            </div>
            <div className="price">
                <div className="figure">{data.price}</div>
                <div className="period">par mois</div>
            </div>
            <ul className="details">{
                data.details.map(function(item, key){
                    return <li key={key}><i className="fa fa-check" />{item}</li>
                })}
            </ul>
        </li>
    }
});

Module.contextTypes = {
    store: React.PropTypes.object
};

function mapStateToProps(state) {
    return {
        data: state.updateModule.data
    }
}

export default connect(mapStateToProps)(Module)

export default Module;

My action :

export function togglePriceModule(status) {
    return {
        type: TOGGLE_PRICE_MODULE,
        activated: status
    }
}

My reducer :

import { TOGGLE_PRICE_MODULE, INCREASE_PRICE_OPTION, DECREASE_PRICE_OPTION } from '../constants/constants.js'

export function updateModule(state = {}, action) {
    console.log('updateModule reducer called');
    if(action.type === TOGGLE_PRICE_MODULE) {
        return {...state, activated : action.activated };
    }
    return state
}

export function updateOption(state = {}, action) {
    if(action.type === INCREASE_PRICE_OPTION) {
        return {...state, value: state.value + 1};
    } else if(action.type === DECREASE_PRICE_OPTION) {
        if (state.value < 2) {
            return {...state, value : 1};
        } else {
            return {...state, value : state.value - 1};
        }
    }
    return state
}

Firstly dispatch is a store function. You need to have the store as a reference, and then import your action and dispatch it. The reducer will handle the logic and return the new state which will trigger a render.

Add this:

Pricing.contextTypes = {
    store: React.PropTypes.object
};

And you should have the store reference.

Then just import your action:

import myAction from './myPath'

Then by doing:

this.context.store.dispatch(myAction(myVar));

That will trigger the dispatch which will return the new state and trigger a render.

For example:

handleClick() {
    this.context.store.dispatch(myAction());
}

and inside render:

<a onClick={this.handleClick}>test</a>

I am using the ES6 syntax there.

Basically the process should be quite straightforward unless I am missing something from your question.

Alternatively, if you console.log(this.props) if you see dispatch there you can just:

  this.props.dispatch(myAction(myVar));

Answering this two questions of yours: How should I make the link between the reducer and the action ? Should I use mapStateToProps and connect somehow ?

Yes you have to import connect in your component to make the link with the store:

import { connect } from 'react-redux';

And yes, you will want to map the state to the props:

function mapStateToProps(state) {
    return {
        myVar: state.myReducer.myVar
    }
}

and finally wrap everything together using connect.

export default connect(mapStateToProps)(Pricing)

Regarding Edit 2:

In your onClick method, you're dispatching an action (via handleClick ) which mutates state. Any time you update state, a re-render is triggered (by invoking the render method). If you update state from within the render method, you could potentially have an infinite loop of re-renders. That's what the error message is complaining about.

Also, onClick is expecting a function as an argument. You can partially apply the function by writing onClick={this.handleClick.bind(this, !data.activated)} .

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