Rendezvous with Redux

node v6.17.1
version: 1.0.0
endpointsharetweet
Let's start by getting a sense of the lay of the land. Redux, for all of its power, has a relatively small API footprint.
const redux = require('redux'); redux;
Yup, just five functions. But, if we want to split hairs. It's actually less than that. `compose()` isn't exactly Redux-specific. It's just a helper function. `compose` takes a series of functions as arguments and returns a new function that applies those functions from left-to-right (or, from last-to-first if you're like me and have trouble discerning right from left).
const makeLouder = (string) => string.toUpperCase(); const repeatThreeTimes = (string) => string.repeat(3); const embolden = (string) => string.bold(); const makeLouderAndBoldAndRepeatThreeTimes = redux.compose(embolden, repeatThreeTimes, makeLouder); makeLouderAndBoldAndRepeatThreeTimes('hello');
So, now for those of you keeping tract at home, we're now really talking about four functions. That's write, we've already covered about 80% of the API and we're just getting started. Covering compose() first was kind of cheating. It's not really super important to understanding Redux. But, we'll take our small wins when we can get them, right? Redux is a friendly state management library. In this industry, we tend to call the place where we keep our state the "store." Redux's createStore() method will … create … a … store.
// let store = redux.createStore();
Hmm… so, I had to comment that out because it definitely didn't work. If you uncomment it, you'll see the error that reads "Error: Expected the reducer to be a function." createStore() expects its first argument to be a reducer. We need a reducer. What is a reducer? No, let's stop for a minute and talk about Redux at a high level. We have the state of our application. We have things that happen (e.g. user actions, WebSocket messages, etc.). When a thing happens, what effect does that have on the state of our application? That's the job of the reducer. It looks at the new thing that happened and it looks at the current state of the world and returns a new state of the world. Here's the crazy thing: it's just a function. It takes two arguments: the thing that just happened and the current (soon to be previous) state of the world. It returns one thing: the new state of the world. To get things started, we're going to make a simple calculator. The state of the world will be an object that stores the current result, which will default to zero.
const initialCalculatorState = { result: 0 }; let somedayACalculatorReducer = (state = initialCalculatorState, action) => { return state; }
So, this reducer doesn't really do anything. It takes the current state of the world and an action. It then ignores the action and just returns the current state of the world. I'd love to implement more right now, but we'll first need to figure out what an action is. TL;DR: an action is a plain old JavaScript object. In practice, we tend to have some conventions as to how this works. Everything I'm about to say should be prefixed with "This is not required, it's just what we all tend to do because it's probably the best way to do this but you totally don't have to." Actions typically have a "type" property. This makes sense, right? An action occured! You're first question will probably be "Well, neat. What type of action occured?" After that, it's up to you and your application what other bits of relevant data there are, right? But this is me and my calculator application—so, I make the calls around here. Let's start with addition. We'll say that the type of the action is "ADD" and then there will be a value, we'll add that to the the "result" that's currently stored in state. Neat. Basically, inside of our store, we need to write the logic that looks at the type of action that just came in and then figure out what exactly that means for the state of our application.
// Imagine we have an action like this: { type: 'ADD', value: 2 } let dontUseThisCalculatorReducer = (state = initialCalculatorState, action) => { if (action.type === 'ADD') { state.result = action.value // This is bad. Don't actually do this. } return state; }
See that comment up there where I say not to this? I mean it. Don't do it. Mutating state is bad news bears for a lot of reasons. But, let's start with an easy one. When you change an object, it's the same object in memory. So, it's really hard to tell that the object has changed. On the other hand, if you totally replace it, then you know it's different. Let's refactor.
// Imagine we have an action like this: { type: 'ADD', value: 2 } let calculatorReducer = (state = initialCalculatorState, action) => { if (action.type === 'ADD') { return Object.assign({}, state, { result: state.result + action.value }); } return state; }
Cool, so we have a reducer now. It will make a copy of the state and add in the value associated with the action that came in. It will then return the new state of the world. Let's wire it up with our store, which will hopefully not blow up this time.
let store = redux.createStore(calculatorReducer);
Okay, cool. So we have a store now. It looks like it has four methods as well. Let's start with getState().
store.getState();
Ah, it's our initial state: { result: 0 }. Nice. This raises a question: how do actions make it to the reducer? Answer: we dispatch them.
store.dispatch({ type: 'ADD', value: 2 }); store.dispatch({ type: 'ADD', value: 1 }); store.getState();
Neat, so we have some state management now. We have taken getState() and dispatch() for a spin. What about subscribe()? It can be tedious to keep asking the store what it's current state is. It'd be better instead to just have Redux tell us whenever the store has been changed.
const subscriber = () => console.log(`Subscriber!' ${store.getState().result}`); let unsubscribe = store.subscribe(subscriber); // The result is currently 3. store.dispatch({ type: 'ADD', value: 100 }); store.dispatch({ type: 'ADD', value: 1000 }); unsubscribe();
In the example above, we're notified whenever the store has changed. This might be useful if we ever wanted to tell some kind of view layer framework that the world is different now. You could also do squirrelly stuff like catch the current state into localStorage or change query params at this point or something like that. We only have one piece of state, but ideally, this would be entire state of the application (assuming you were wise enough to keep all of your state in Redux, that is). Speaking of having more things in state, how do we do that? You could make a giant reducer. You shouldn't make a giant reducer. You _shouldn't_ make a giant reducer, but you could. It's your life. I'm not your real dad. What we typically do is break things out into logical reducers and then combine them when we create the store. (Careful readers will remember that there was a combineReducers() method at the top of this very important document.)
// We already have calculatorReducer above. const initialErrorMessageState = { message: '' }; let errorMessageReducer = (state = initialErrorMessageState, action) => { if (action.type === 'SET_ERROR_MESSAGE') return { message: action.message }; if (action.type === 'CLEAR_ERROR_MESSAGE') return { message: '' }; return state; }; let combinedReducers = redux.combineReducers({ calculatorReducer, errorMessageReducer }); const storeWithErrorSupport = redux.createStore(combinedReducers); storeWithErrorSupport.dispatch({ type: 'SET_ERROR_MESSAGE', message: 'This aggression will not stand.' }); storeWithErrorSupport.getState();
It now stores both, but this is a little weird. When we use combineReducers() stuff is namespaced under whatever we called the reducers. We're using the new object notation that names keys and values after the variable name. Let's play with this a bit and see how it goes.
storeWithErrorSupport.replaceReducer( redux.combineReducers({ calculator: calculatorReducer, error: errorMessageReducer }) ); storeWithErrorSupport.getState();
We also got a chance to take store.replaceReducer() for a spin. We could probably get rid of the objects here and just store the raw values in here, but I'll leave that as an exercise for the reader. Let's take about the fact that we're passing in objects as actions. This is a little perilous because if we get the object wrong, it's unclear what will happen (probably nothing). We tend to use a pattern called "action creators." Like most things in Redux, this is simpler than it sounds. It's just a function that returns an action object.
const add = (value) => ({ type: 'ADD', value }); storeWithErrorSupport.dispatch(add(4)); storeWithErrorSupport.getState();
You'll also see people protect themselves from fat-fingering the action type by making constants.
const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; const CLEAR_ERROR_MESSAGE = 'CLEAR_ERROR_MESSAGE'; const setError = (message) => ({ type: SET_ERROR_MESSAGE, message});
The idea here is that if you mess up a string, nothing will blow up. Your code will just quietly not work. But, if you misspell a constant you'll see an error message alerting you to your horrible mistake. Onward to our next problem: Imagine we're using React. Just imagine it for a moment. Do we want to pass the entire store to every component? That's a recipe for bad decisions. What we'd like to do is pass a function that knows about the store already. Then we can just call that function and the action will be dispatched to the store. React can stay blissfully unaware that Redux even exists.
const errorActions = { set: setError, clear: () => ({ type: CLEAR_ERROR_MESSAGE }) } const errors = redux.bindActionCreators( errorActions, storeWithErrorSupport.dispatch );
Okay, let's take it for a spin. This will create an object with methods that know about the action creators as well as the store and how to dispatch to it.
errors.set('I AM BOUND!'); storeWithErrorSupport.getState();
Okay, so we have a problem. All of this so far is totally synchronous. But, a thing that we do as web developers all of the time is make asynchronous requests to servers and whatnot. Our lowly actions are just objects. They're not up for the job. Enter: redux-thunk. A thunk is a function that will eventually return an object. Kind of like promise, I guess. What the redux-thunk does is dispatch a function, which—when it is done doing it's thing—eventually dispatch an action to the store. If you look closely, makeBelieveAjaxRequest() returns a function. That function does the asynchronous this. In this case it's a setTimeout(), but it could totally be an AJAX call. When that timer is done or the AJAX calls come back, we _then_ will dispatch the action to the store. The `dispatch` argument passed to the inner function is the dispatch method of the store that makeBelieveAjaxRequest was used with.
const reduxThunk = require('redux-thunk').default; let makeBelieveAjaxRequest = () => { return dispatch => { setTimeout(() => { dispatch(add(100000000)); }, 1000); }; } const reducers = redux.combineReducers({ calculator: calculatorReducer, error: errorMessageReducer }) const storeWithThunkAbilities = redux.createStore( reducers, redux.applyMiddleware(reduxThunk) ); storeWithThunkAbilities.subscribe(() => { console.log(storeWithThunkAbilities.getState().calculator); }); storeWithThunkAbilities.dispatch(makeBelieveAjaxRequest());
That's all we have time for today, but in our next installment, we'll look at the container pattern and how to wire up your Redux store to your React application use the `react-redux` library.
Loading…

no comments

    sign in to comment