A simple project for getting my team up to speed with our new technologies.
To start this workshop, you have two options:
-
Pull this in locally with
git clone [email protected]:cerebralix/react-redux-workshop.git
and then rungit checkout 343d1abac5ae57acb03b7adbeceb8238c7af2a98
to grab the starting point for the project.IMPORTANT: I recommend that you don't edit this project, but create your own and just have this as a reference. I will be force pushing changes to this project, so it will most likely cause issues if you edit this.
-
Or, just keep this open in Github and just navigate the code via the commit history.
At each step, I will provide you a link to the commit that provides the answer, or you can checkout the commit here for the code.
-
In the core React and ReactDOM libraries (what we linked above), you have a few methods off of the exported object that you have to know:
React.createElement
ReactDOM.render
-
Using these two methods, create a simple "Hello, World!" react component.
React.createElement('h1', null, 'Hello, World!');
-
Now, render this component to the DOM:
ReactDOM.render( YOUR REACT ELEMENT HERE, document.getElementById('root') );
-
Visit the page in the browser to see if it works!
-
Now, I want you to have the
<h1>
element not contain the string, but contain another React component that contains the string. -
Also, make the child React component render the a
<span>
with inline CSS that makes the color of the text red. -
Finally, pull "World" out of the string and add it back as a JS variable by concatenating it with "Hello, " and "!".
That's it! You're now done with Step One. Here's the commit on Github.
The important thing is to know that all the fancy JSX that you see that looks like a cross between HTML and XML is converted (or transpiled) to what you just wrote in Step One. What you write next in JSX is nothing more than a fancy way of writing plain old JavaScript.
-
Add the JSX tranformer to your index page after the two existing scripts:
<script src="https://fb.me/JSXTransformer-0.13.3.js"></script>
-
Now convert your
view.js
file toview.jsx
. This tell the JSX transformer to transpile the JSX to JavaScript at runtime. -
Go back to your
index.html
file and update the script tag to pull the.jsx
file now and also add thetype="text/jsx"
attribute to the<script>
tag -
You're now ready to write JSX!
-
Instead of writing
React.createElement
everywhere, you can now just write a function that returns JSX, and use that. Here's an example:var Greeting = function Greeting() { return ( <span>"Hello, my name is Justin."</span> ); };
-
Then you can import that into another component, or the
ReactDOM.render
method like so:ReactDOM.render( <Greeting />, document.getElementById('root') );
-
Go ahead and convert your "Hello, World!" app to this style. It's important to note that
-
Giving that should have worked for you. Let's get a little fancier. React allows the use of programmatic classes that provide more power to the tool. Let's start by converting our function to a class like so ...
var Greeting = React.createClass({ render: function render() { return ( <span>"Hello, my name is Justin."</span> ); } });
-
Nothing will need to change in your
ReactDOM.render
method, so refresh your app in the browser, and all should work! -
Let's now add two more properties to the class.
getInitialState
, which is an official property of the class. It does exactly what it says and is part of the React lifecycle. The other is a function that captures a form submission. It should look like this:var Greeting = React.createClass({ getInitialState: function getInitialState() { // ... }, setNewName: function setNewName(event) { // ... }, render: function render() { return ( <span>"Hello, my name is Justin."</span> ); } });
-
Now that we have the ability, add your initial name, "World", to the initial state by returning the string inside an object in the function.
-
setNewName
method will capture a formonsumbit
event, so it needs topreventDefault
. Then, run athis.setState()
with the new name as an argument. You can get this argument off theevent.currentTarget
, which I'll explain shortly. This method will be available off of thethis
object. -
Finally, add the necessary JSX (basically the HTML) of the form:
<form onSubmit={this.setNewName}> <label for="name">Who do you want to say hi to?</label> <input id="name" type="text" defaultValue={this.state.name} /> <input type="submit" /> </form>
-
Now, that
onSubmit
fires off thesetNewName
method. Notice how we are setting a default value withthis.state.name
? -
We can now set the new name with the following bit of code in the
setNewName
method (make sure to not forget theevent.preventDefault()
call as well):
this.setState({ name: event.target.querySelector('#name').value });
- Does it work? Keep trying and see if you can get it working. Don't forget to utilize the React documentation for the API. Don't just give up and look at the answer
That's it! You're now done with Step Two. Here's the commit on Github.
By now, you probably have a very large class that houses all of your app. Start splitting it up! Have your root class import in another React element so you start composing with components:
-
Create a new functional React component call
Form
and have it return all your form JSX:var Form = function form(data) { return ( <form onSubmit={data.submitMethod}> <label htmlFor="name">Who do you want to say hi to?</label> <input id="name" type="text" defaultValue={data.state.name} /> <input type="submit" /> </form> ); };
-
Now import that form in your root app component:
<div> <Form /> <h1>Hello, {this.state.name}!</h1> </div>
-
This isn't going to work. That's because the form component doesn't have access to the needed data and functions. Let's add that now!
-
When importing the
<Form />
component in, pass the state and form submission function like this:<Form state={this.state} functionName={this.functionName} />
Once you feel your code is organized and working, check out how I did it here.
You now know enough to get started making your first todo app. Here are the requirements:
-
A form needs to be able to accept a string and, on submit, push that string onto an array that represents the state of the app.
-
When the data is pushed to the array, the new state is set with
this.setState()
, that's how the app knows to re-render. The array should look something like this:['Learn React and Redux']
-
The app maps over the new array creating an unordered list of todos. The trick here is to do something like this:
<ul> {props.todos.map(function (todo, i) { return <Todo todo={todo} key={i} />; })} </ul>
It may be hard, but really try to get this. If you're stuck, check out how I do it here, but don't just plain old copy and paste.
Moving state out of the view is critical for separating of concerns of software development. Redux allows us to put our data in a single store, and provide that data to the view at render time.
-
Add Redux (store and data API) and React-Redux (wrapper to connect Redux to React components) to your project. Add these to your index.html:
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.3.1/redux.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.1/react-redux.js"></script>
This provides
Redux
andReactRedux
to the globalwindow
object. -
Now create a
store.js
file and add a script tag referencing it to your index.html as well. This,store.js
is where we'll place all of our data related code. This script tag needs to go before yourview.jsx
script tag. -
We now need two things:
- Our first "reducer"
- Our store
-
A reducer is a function that takes in a
state
object andaction
object as parameters, and, through a conditional (usually a switch statement) returns a new state according to the action type:function myReducer(state, action) { switch (action.type) { case 'SAY_HI': return 'hello'; case 'SAY_BYE': return 'bye bye'; default: return state; } }
The reducer your going to write needs to evaluate the
action.type
for an"ADD_TODO"
string. If so, return a NEW array with the additional todo. And, if the action is not"ADD_TODO"
, then just return the existing state.Remember, when using Redux, you can't mutate the state, you can only return new state, so don't accidentally use references.
-
Now, create your store with this reducer, and provide an initial state array:
['Learn React and Redux']
:window.store = Redux.createStore(todoReducer); // or, whatever you called your reducer
-
Now that you have a store, and you've added it to the global
window
object, use it in yourview.js
instead of having it manage it's own state. To do this, you will modify yourgetInitialState
method and have it callstore.getstate()
and modify your method you've attached to youronSubmit
event to callstore.dispatch()
with your suppliedaction
object. After calling the dispatch method, you then have to setState to the view to tell React to re-renderthis.setState()
callingstore.getState()
again to supply the data. Then, ensure that you clear the input after adding the new todo.This is what a
store.dispatch()
should look like:store.dispatch({ type: 'SAY_HI' });
You will also have to pass any data within the object as well, so you may have a key of 'todo' or 'title' and pass the string value along as well.
-
Now, get your app working again! Make sure you can add many todos and that the input clears itself after the submit.
Once you done, check out how I do it here
Data is very rarely represented as an array of string, so let's make this more challenging. Let's make it an array of objects, each object representing a todo.
-
Each todo object is going to need three properties:
- an ID which should match the index of the array position
- the text or title of the todo
- whether the todo is completed or not
-
First, make a quick change to you add todo condition so that it returns an id for the newly added todo. You can just use the length property on the array.
-
Now, you need to be able to complete a todo. This requires a few things:
- Each todo that's printed to screen needs to have a checkbox that represents whether the todo is done or not
- A new condition within the reducer that accepts the new action of completion, and returns a new, not mutated, state
- A new onChange event handler for the checkbox to capture the event and trigger another dispatch.
-
So, first, write your new reducer condition. This is actually challenging, and it's intentional, as I want you to feel how difficult it is to write non-mutating state functions. A hint will be split the array into pieces create your new todo object within the array and concat them all back together again. If you get stuck for too long, just look at my solution.
-
Next, add the event handler to your view for the
onChange
event associated with yourcheckbox
, and do astore.dispatch()
with your action object and then set the state on the view to trigger a re-render.To make this a little more usable, wrap your checkbox and text in a
label
element and use thefor
andid
attributes to link them together. -
Conditionally set a class on the todo's
<li>
if it's completed. So that it is styled as "completed". Be sure to grab the CSS styles below and place them in your CSS file:body { margin: 2em; } input[type="text"] { display: block; margin-bottom: 1em; } input[type="checkbox"] { margin-right: 1em; } .completed { text-decoration: line-through; color: lightgray; }
-
Lastly, and to help with debugging, leverage the
store.subscribe()
method like this to see your data change in real time:store.subscribe(function logStore() { console.log(store.getState()); } );
This method allows you to run a function on each new store change. In this instance, it just calls a function that logs to the console.
-
Check your work. Does it allow you to add todos and complete and uncomplete them?
Step Six's solution here to check your work
Rather than managing the two states of our app, Redux store and React's this.state, let's have something connect the two entities together for us!
So that I don't have to explain the inner workings of ReactRedux, please watch these amazing tutorials with Dan Abromov on Egghead.io.
The basic components to know are:
- Provider
- connect
- mapping state to props
- mapping dispatch to props
-
Ensure that you're pulling in ReactRedux via the
<script>
tag on the index.html page. -
What we want to do is pull state and event related functions out of our view and delegate that to our ReactRedux library.
-
Within you main jsx file, pull your state related functions out and place them in a function called "mapStateToProps". This is what it should look like:
function mapStateToProps(state) { return { /* your state object */ }; }
-
Now, pull you event listener functions out of your React component and place them in a function called "mapDispatchToProps". Like this:
function mapDispatchToProps(dispatch) { return { /* your event methods object */ }; }
-
Using ReactRedux, call the
connect
method passing in the two map functions as arguments. Then, using partial application, call it again, but now with your root React component as the only argument, making sure your save what it returns to a variable:var ConnectedApp = ReactRedux.connect( mapStateToProps, mapDispatchToProps )(/* Root React Component */);
-
The above returns a connected app that will now listen for state changes for you, so there's no longer a need for the React.createClass and the
this.setState
orgetInitialState
methods. Let's now write our React components as stateless, pure functions. -
Finally, wrap your new
ConnectedApp
component withReactRedux.Provider
and pass the Redux store you created earlier into a attribute called store:ReactDOM.render( <ReactRedux.Provider store={ /* Your Redux store reference */ }> <ConnectedApp /> </ReactRedux.Provider>, document.getElementById('root) );
-
Clean up your code and see test. Remember, we want all our React views to be stateless, pure functions. Let the libraries do the work for us.
How can we break this up into functional pieces? What about the classic MVC? Or, how about components, containers and store? Or ... how would you split these into different files? This is a do-your-own-thing exercise.
Let's add TypeScript to give us a better development environment! First things first:
-
Install the needed things: TypeScript and Webpack. The former will transpile our code, among other things, and the latter will build our bundle for the browser.
npm install typescript webpack --save-dev
-
Now that we have TypeScript, we'll need the type definitions for our libraries. To do this, the best way is to do a
git clone
on DefinitelyTyped. After this installs, you'll have almost every major libary and framework definition you'll ever need :) -
Copy and paste the following directories to a directory called
typings
ordefinitions
in your project from the DefinitelyTyped project:- react
- react-redux
- redux
-
You'll now need a way to reference this definitions. So, at the root of your application, create a new file called
typings.d.ts
or whatever, and reference each library definition with the appropriate syntax:/// <reference path="react.d.ts" /> /// <reference path="react-dom.d.ts" /> /* So on and so forth */
-
Now install the libraries:
npm install react react-dom react-redux redux --save
-
You can now switch all your files to
.ts
or.tsx
extensions (similar to.js
or.jsx
) and ES6 modules. Don't change too much of your code, but just embrace the new module definition. A part of this will be removing all the references to the global window object for your library methods. You'll get them by pulling them in now. -
Delete all the
<script>
tags in yourindex.html
except one, and call thatbundle.js
. -
After you've switched them all over and removed the unneeded script tags, you are almost ready to do the first step in your build. Create a
tsconfig.json
file for your TypeScript configuration:{ "compilerOptions": { "target": "es5", "module": "commonjs", "moduleResolution": "node", "jsx": "react", "noImplicitAny": false, "sourceMap": true }, "exclude": [ "node_modules", "typings" ] }
-
Now you need an NPM command to run it. In your
package.json
add a script to run called "transpile":"scripts": { "transpile": "tsc" }
-
Type
npm run transpile
in your terminal. This should build your files without error. If you get an error, ask you instructor for help. Or, use the Googles :) -
Last but not least, we need to bundle our files into something the browser can execute. So, add another script to the
package.json
to run a bundle method using Webpack:"scripts": { "transpile": "tsc" "bundle": "webpack ./app.tsx bundle.js" }
-
Run
npm run bundle
in your terminal. This should produce a single file calledbundle.js
. -
Refresh your app in your browser and everything should work :)
NOTE: You'll probably want to ignore all the built files with .gitignore
.
Let's organize and atomize our app's components. This will help prevent our root directory from exploding with files. Here's the naming of our directories:
- views
- initiators
- store
- reducers
- styles
Now that we have TypeScript, let's use one of its most powerful features: interfaces and typings.
-
Let's define what the interface is for our state and actions, and what our reducer returns within our store.js file. If you are unfamiliar with how to do this, reference the TypeScript docs, but here's a shell you can start with:
interface Todo { /* describe the shape of your todo item */ } interface State { /* describe the shape of your todo item */ } interface Actions { /* describe the possible shape of actions */ /* this will likely have optional params since it should describe all possible actions */ example?: string; // the question mark denotes optional }
-
Now, within your reducer function, assign the correct types to the function parameters, using the colon syntax, and for the return object:
function myFunc(paramOne: InterfaceOne, paramTwo: InterfaceTwo): InterfaceForReturn {};
-
If you haven't already done it, make sure your actual state object looks like this:
{ todos: [] }
. If it's just returning an array, change this now. Don't forget to update yourmapStateToProps
method if you changed the store. -
You are more than likely going to encounter errors from TypeScript, you have to really think about what the errors are telling you. Most likely, it's when TypeScript tries to infer a type, and get's it wrong. Array.concat() is going to be one of those. So, make sure to pass in similar types into
concat
and not mixed types.
We want our views to be as simple as possible. With the sole responsibility of rendering to the page with given data.
- Our next step in doing this is pulling the event methods out of the app.tsx, and into a separate module called
client-events.ts
. - Now, rather than continuously pass ... coming soon ...