The goal of this assignment is to learn about Redux and Props.
Start in the root of your main (isit320-lastname-2017) repository. Build a create-react-app project called Week08-ReduxBasics:
create-react-app week08-redux-basics
Navigate into your new project and install redux:
npm install --save redux react-redux redux-devtools
In Redux projects, it is often best to:
There are several pieces in application of this type. One is a module called a reducer.
There is only one place where you rebuild state: the reducer. Hence there is never any question of where something happened in a big application. It could only have happened in the reducer. I explain reducers later in this document.
Redux tracks your app's state. It has the following methods:
They are discussed below in various different sections of the document.
We change state by "dispatching" an action. (You can log, serialize or store actions.)
Here is a typical call to dispatch:
dispatch({
type: 'GIT_USER',
gitUser: gitUser
});
We name the action by giving it a type. And in this case, we have optionally included some data that will be used in a reducer when it rebuilds state.
Redux maintains your application's state in something called a store.
import {createStore} from 'redux';
let store = createStore(spokesman);
I write this code, which dispatches an action:
store.dispatch({type:'ALL', users });
In your reducer, action.users is the thing you dispatched: {type: ALL, users}.
The usual flow:
That's the end of the flow. Summary
In the next few sections we will put together an example.
Lets begin by designing some simple actions:
let verify = { type: 'VERIFY' }
let deny = { type: 'DENY' }
let noComment = { type: 'NO COMMENT' }
Note that there is no data associated with this actions. This means the actions are simple enough that they can re-build state without any additional information other than the action itself.
This we will use in serveral places, so save it as src/spokesman.js:
const spokesman = (state = { statement: 'No comment' }, action) => {
switch (action.type) {
case 'VERIFY':
return { statement: 'We stand by it. In fact, we invented it.' };
case 'DENY':
return { statement: 'We deny everything. We have never heard of it.' };
case 'NO COMMENT':
return {statement: 'No comment.'};
default:
return state;
}
};
export default spokesman;
The spokesman function takes two parameters:
This function has a very simple state object:
state = { statement: 'No comment' }
That's it. The statement is the only moving part in the application. There are other variables, but their scope is limited to the object in which they are declared. The state object, however, has broader scope. In short, there are no other variables in the application that can change that have any effect outside of the component in which they are declared.
For instance, in a classic for loop, we often declare a variable called i:
for (let i = 0; i < 3; i++) {}
We don't need to put i in our application's state since it is not used by any other components. In fact, it is only used in one place in one method. So yes, the variable i is declared, and it can change value, but it is not part of our applications state. It's scope is very limited.
But the state object in spokesman is different. It is used by multiple modules, or at least we are pretending it is the type of data will be used in various places throughout the app. (This is, after all, an example, an academic exercise meant to teach principles. It is not meant perform a useful function in the real world in the sense that WebCrafts does.)
Our reducer modifies statement by completely rebuilding state. This is the only place in the application where statement can be modified.
In short, state is immutable. It can never be changed in any part of the programmer. We can send ask that state be rebuilt in the reducer, but we can't modify state on the fly in other parts of the app.
Here is an example call to dispatch:
store.dispatch({ type: 'VERIFY' });
This message is dispatched to our reducer. The reducer responds by modifying the state like this:
{ statement: 'We stand by it. In fact, we invented it.' };
When the state changes, several parts of your app might want to be notified. You track changes state by monitoring calls to subscribe():
import {createStore} from 'redux';
let store = createStore(spokesman);
store.subscribe(() => {
this.setState((prevState) => {
const storeState = this.props.store.getState();
return {statement: storeState.statement}
});
});
Now you can modify the UI based on the changes.
At heart, Redux is very simple. Consider this fake Redux implementation. It is not Redux itself, but it is Redux-like. Save it as src/FakeRedux.js:
import React, { Component } from 'react';
import spokesman from './spokesman';
class FakeRedux extends Component {
state = spokesman(undefined, {});
dispatch(action) {
this.setState( (prevState) => spokesman(prevState, action));
}
verifyStatement = () => {
this.dispatch({ type: 'VERIFY' });
};
denyEverything = () => {
this.dispatch({ type: 'DENY' });
};
noComment = () => {
this.dispatch({ type: 'NO COMMENT' });
};
render() {
return (
<div className="App">
<h1>Political Science Fake Redux</h1>
<p>This component does not use redux. It uses something redux-like.</p>
{this.state.statement}
<hr />
<button onClick={this.verifyStatement}>Verify</button>
<button onClick={this.denyEverything}>Deny</button>
<button onClick={this.noComment}>No Comment</button>
</div>
)
}
}
export default FakeRedux;
In index.js, we want to do two things:
In this code we are also displaying our FakeRedux file, but it has nothing to do with implementing the real Redux. We only display it so that we can see it in action.
Keep the declarations for React etc, but past this in as the working part of your src/index.js file:
import spokesman from './spokesman';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import FakeRedux from './FakeRedux';
let store = createStore(spokesman);
ReactDOM.render(
<div>
<Provider store={store}>
<div>
<App />
<FakeRedux/>
</div>
</Provider>
</div>
, document.getElementById('root'));
Note that we are completely replacing the default call ReactDOM.render.
In its pure form, each component that wants to access Redux can do so on its own, without looking at props, and without peering into any other modules. But you can, if you want, pass down the Redux state in props. This is not the "right" way to do Redux. Yet, on the other hand, it is not wrong. Redux provides much more flexibility than a monolithic tool like Angular. So if you want to pass down state with props, go ahead and do it. But there is a "better" way, which will be explained later in this document.
Save this object into "src/App.js":
constructor(props) {
super(props);
this.state = {
statement: 'No comment'
};
this.props.store.subscribe(() => {
const storeState = this.props.store.getState();
this.setState((prevState) => {
return {statement: storeState.statement}
});
})
}
verifyStatement = () => {
this.props.store.dispatch({ type: 'VERIFY' });
};
// IMPLEMENT denyEverything AND noComment HERE.
render() {
return (
<div className="App">
<h1>Political Science Props Redux in App</h1>
<p>This component uses redux.</p>
{this.state.statement}
<div className="Box">
<button onClick={this.verifyStatement}>Verify</button>
<button onClick={this.denyEverything}>Deny</button>
<button onClick={this.noComment}>No Comment</button>
</div>
<hr />
</div>
);
}
Notice that we do everything Redux-related on the store object passed as props from index.js:
const storeState = this.props.store.getState();
this.props.store.dispatch({ type: 'VERIFY' });
To make the above work, your index.js might look like this:
ReactDOM.render(
<div>
<Provider store={store}>
<div>
<App store={store}/>
<hr /> <hr />
<FakeRedux />
</div>
</Provider>
</div>,
document.getElementById('root'));
So what is the better way? What do we do if we want to create a component that does not get props, but does have access to the Redux data store?
Create a new file called src/ComponentConnect that is based on src/App.js. Be sure to rename the React component to match the name of the file.
Import connect at the top of the component that we want to give access to the Redux store:
import {connect} from 'react-redux';
This time we have no subscribe in the constructor:
constructor(props) {
super(props);
this.state = {
statement: 'No comment'
};
}
In fact, we don't really need the constructor at all, and you can delete it.
Now we use connect to connect our component to the Redux data store. We do this by adding the following code at the bottom of the file and outside of the component:
const mapStateToProps = (state) => {
return {
statement: state.statement
}
};
ComponentConnect = connect(mapStateToProps)(ComponentConnect);
The method called mapStateToProps is passed to connect as its first parameter. It is our chance to map the applications state found in spokesman to the props found in our component. We get the Redux (spokesman) state in our props, so we are mapping the Redux state to our component's props.
More information is here.
We no longer get store in the props passed from index.js. Here was the old code:
verifyStatement = () => {
this.props.store.dispatch({ type: 'VERIFY' });
};
And here is the new code that does not rely on a store. Instead, dispatch is build in because we use connect:
verifyStatement = () => {
this.props.dispatch({ type: 'VERIFY' });
};
Finally, in your JSX, use this.props.statement rather than this.state.statement.
NOTE: This is one of those occasions when I am not listing explicitly every change you need to make. Hopefully this is enough to help you see what to do, but it more than just a cut and paste operation.
Here is code that uses a method of connect called mapDispatchToProps. This allows us to define simple dispatch methods that are triggered by button clicks:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import {connect} from 'react-redux';
// We change state by "dispatching" an action.
// You can log, serialize or store actions.
let AppConnect = ({statement, deny, verify, noComment}) => {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo"/>
<h2>Welcome to React and Redux</h2>
</div>
<p className="App-intro">
This AppConnect component uses Redux and connect.
</p>
<h1>Political Science</h1>
{statement}
<div>
<button onClick={verify}>Verify</button>
<button onClick={deny}>Deny</button>
<button onClick={noComment}>No Comment</button>
</div>
<hr />
</div>
);
//}
};
const mapStateToProps = (state) => {
return {
statement: state.statement
}
};
const mapDispatchToProps = (dispatch) => {
return {
deny: () => {
dispatch({type: 'DENY'})
},
verify: () => {
dispatch({type: 'VERIFY'})
},
noComment: () => {
dispatch({type: 'NO COMMENT'})
}
}
};
AppConnect = connect(
mapStateToProps,
mapDispatchToProps
)(AppConnect);
export default AppConnect;
Notice that we are using an arrow method rather than a component, and notice that it takes parameters, filled in by React, that represent the various methods and properties used by Redux.
Here is the arrow function:
let AppConnect = ({statement, deny, verify, noComment}) => { ... }
And here where we use them in our JSX:
{statement}
<hr />
<button onClick={verify}>Verify</button>
<button onClick={deny}>Deny</button>
<button onClick={noComment}>No Comment</button>
We can now break out our code into two pieces:
Here is the JSX only part. Save it in TwoPartJsx:
import React from 'react';
import './App.css';
let TwoPartJsx = ({statement, deny, verify, noComment}) => {
return (
<div className="App">
<h1>Two Parts: Code and JSX</h1>
<p className="App-intro">
This AppConnect component uses Redux and connect.
The connect bits are in a separate files.
</p>
{statement}
<div className="Box">
<button onClick={verify}>Verify</button>
<button onClick={deny}>Deny</button>
<button onClick={noComment}>No Comment</button>
</div>
<hr/>
</div>
);
};
export default TwoPartJsx;
And here is the connect only code. Save it in TwoPartCode:
import {connect} from 'react-redux';
import TwoPartJsx from './TwoPartJsx';
const mapStateToProps = (state) => {
return {
statement: state.statement
}
};
const mapDispatchToProps = (dispatch) => {
return {
deny: () => {
dispatch({type: 'DENY'})
},
verify: () => {
dispatch({type: 'VERIFY'})
},
noComment: () => {
dispatch({type: 'NO COMMENT'})
}
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TwoPartJsx);
The point is that our interface is now separate from our logic. This has two advantages:
Suppose we wanted to update our code to use slightly more complex state that has two properties rather than one.
const spokesman = (state = { statement: 'No comment' }, action) => {
switch (action.type) {
case 'VERIFY':
return {
statement: 'We stand by it. In fact, we invented it.',
kind: 'verify'
};
case 'DENY':
return {
statement: 'We deny everything. We have never heard of it.',
kind: 'deny'
};
case 'NO COMMENT':
return {
statement: 'No comment.',
kind: 'no Comment'
};
default:
return state;
}
};
export default spokesman;
And then the show the new property in TwoKindJsx.js:
import React from 'react';
import './App.css';
let TwoPartJsx = ({statement, kind, deny, verify, noComment}) => {
return (
<div className="App">
<h1>Two Parts: Code and JSX</h1>
<p className="App-intro">
This AppConnect component uses Redux and connect.
The connect bits are in a separate files.
</p>
<p><strong>Kind</strong>: {kind}</p>
<p><strong>Statement</strong>: {statement}</p>
<div className="Box">
<button onClick={verify}>Verify</button>
<button onClick={deny}>Deny</button>
<button onClick={noComment}>No Comment</button>
</div>
<hr/>
</div>
);
};
export default TwoPartJsx;
And modify TwoKindCode to initialize the new property:
const mapStateToProps = (state) => {
return {
statement: state.statement,
kind: state.kind
}
};
By popular demand, here is the final index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Header from './Header';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import spokesman from './spokesman';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import FakeRedux from './FakeRedux';
import DispatchConnect from './ComponentConnect';
import AppConnect from './AppConnect';
import TwoPartCode from './TwoPartCode';
let store = createStore(spokesman);
ReactDOM.render(
<div>
<Provider store={store}>
<div>
<Header/>
<TwoPartCode/>
<AppConnect/>
<DispatchConnect/>
<App store={store}/>
<FakeRedux/>
</div>
</Provider>
</div>, document.getElementById('root')
);
registerServiceWorker();
I put this in App.css:
.Box {
margin-top: 10px;
}
Add, commit, push. Create branch and or tag. Push. Tell me branch, tag and folder when turning in assignment.
In a later assignment we will see that Redux can write the current state to localStorage.
I think the main goal of Redux is to help us maintain state across an entire application. Like so many frameworks, it provides a means of codifying best practices. In this case, some of the benefits include:
It enforces the best parts of the MVC architecture. In particular, it focuses on Separation of Concerns. It maintains state in a centralized location that is very loosely coupled to the view. So M in MVC is the Redux. It maintains our model, our state. The V in MVC in React. It creates our view.
Another best practice that it enforces is immutability. We cannot modify the data maintained by Redux, we can only send it a command, an action, and in response Redux may rebuild our entire state, but it will not modify it.
There are other tricks that Redux can perform, like helping with storing state between sessions. But that is a side benefit. The key benefits are the centralized object with strong separation of concerns, along with the immutable object.