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.