import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import rootReducer from './reducers'; import './base.scss'; import App from './containers/App/App'; ALWAYS IMPORT THE WRAPPED VERSION import { BrowserRouter as Router } from 'react-router-dom' import * as serviceWorker from './serviceWorker';
const store = createStore(rootReducer, composeWithDevTools())
ReactDOM.render( <Provider store={store}> <Router> <App /> </Router> </Provider>, document.getElementById('root'));
We need to build the store here. This is step 0.
Your parent component needs to be wrapped in a Provider component. More magic. But it takes the store as a property! So we can assume that Provider, and its props (the store object) is something all of the other components can communicate with directly.
To make the store, we use a magical function that we take from redux called createStore. Really don't know what it does AT ALL. But at the end of the day, we have a store, that is an object like a state in a component, and uses the functions in rootReducer as key-value pairs. Each of these functions - which are fundamentally if/else statements - resolves to what the state is for that key.
Now, let's make a rootReducer. Generally, this goes in our reducers folder, though I see it as more of a util, really.
import { combineReducers } from 'redux'; import { users } from './users'; import { infos } from './infos'; import { items } from './items'; import { currentUser } from './currentUser.js'; import { chosenItem } from './chosenItem'
const rootReducer = combineReducers({ users: userReducer, infos: infoReducer, items: itemReducer, currentUser: currentUserReducer, chosenItem: chosenItemReducer })
export default rootReducer
This...is actually kind of the most important part of our store. Maybe it even IS the store. This is the part you can think of as our global state. The object that the magical combineReducers() function takes in IS THE GLOBAL STATE. In a normal state, you might have something like:
this.state = { users: [], currentUser: {}, rooms: [], }
This is like that. users: is now part of our global state. Except, instead of assigning it to whatever the state starts in and then using functions, passed around as props, to change it, we assign it to a REDUCER, which is at its root a function whose return is determined by an if/else statement, often a very large one.
This function takes two arguments - the state as it starts, and the action object - spun up by an action creator - and dispatched to it from the app itself.
The function looks at the 'type' property on the action object to tell it what to return. This is where I get on my soapbox - the name 'action' is actually extremely misleading to me. What we're actually dispatching, and what the argument the reducer-function is taking in, is more like a CONDITION. A condition that will satisfy a portion of the if/else function and trigger the appropriate return. That return will then be set as the state.
THEN, (and this is where it gets fancy), the state change will be communicated ONLY TO THE COMPONENTS that you have set to care about this change (it's with mapStateToProps). Only THOSE components will update.
Ok, time to make a reducer. Or update a reducer. Either way, this can either be step 1 or step 4, depending on how you look at things. The way I'm doing this, it's step 4. Just some extra fun from notes:
Provider - Comes from react-redux. According to the react-redux docs, “The Provider makes the Redux store available to any nested components. Since any React component in a React Redux app can be connected, most applications will render a Provider at the top level.” Note that the Provider wraps around the entire app. (if you are using React-Router, it will wrap around BrowserRouter)
createStore - Comes from redux. According to the redux docs, “This creates a Redux store that holds the complete state tree of your app. There should only be a single store in your app.”
composeWithDevTools - A method we brought in and can pass as an argument with createStore so that we have access to our devtools and can view our store. (order matters here)
export const users = (state = [], action) => { switch (action.type) { case "SET_USERS": (make sure to use the spread operator in the return as appropriate) return [...action.userList]; So if the second argument's type property is SET_USERS, the user key's value in the global state will be set to whatever the return is. So make sure you understand exactly what the return is! default: return state; } }
tip: If you want to return the current list of users plus a new list of users, do this: return [...state...action.userList]
Welcome to step 4.
Dispatch says, 'Hey store, I'm going to call knock on your rootReducer, aka Global State, and call at each one of its values - which are functions - with this object I'm holding as its second argument. Every single one of them is going to fall to the default except the one who has a case that matches the 'type' property on that object. Then that key in state's value will reset to whatever the action.type told it to.'
For a deeper look at this, go to index.js and reducers/index to see step 0.
That's why you need a default!
- This is step 1. It can also be step 4. It's in a component. Basically, we have info, and we want that info in our app, and we want it to be easily accessible from multiple components. And so we begin.
getInfos = () => { return getUsers('https://fe-apps.herokuapp.com/api/v1/overlook/1904/users/users', 'users') .then(data => this.props.makeUserList(data.users))
Proceed to step 2 in actions/index. Don't forget - makeUserList is a function, assigned to a prop! We're calling that prop-function here and telling it to call setUser(), the action creator it's associated with, with - FINALLY - an argument, which we pulled from an API.
- Hello.
export const setUsers = (userList) => ({ type: "SET_USERS", userList: userList })
function setUsers(userList) { return {type: "SET_USERS", userList: userList} }
Above is another way to write the action creator. Note, the function is the action creator, and the object itself is the action. I have NO IDEA why it's called an action - literally it's just an object with a type added in. (I address this later, and I think it should be called a condition) You always name the TYPE value to reflect the action you're GOING to take - as in, what you're going to change about the state. In this case, we're going to take an empty state and set it to an array of users - THE array of users. Your info that you decided on in step 1! That's the payload.
Sometimes you might not need a payload. Like for a logout thing.
These are what get dispatched to the global state, the object in rootReducder that combineReducers takes as an argument, and basically thrown at every single value in its key-value pairs as a second argument. It won't DO anything with most function-values because the functions will be looking at the type, and if it doesn't fit, it will go to the default.
This is what I consider to be step 2. Step 1 is getting the info you need to store in the global state.
Step 3 is actually dispatching (or what I like to think of as throwing it against the global store until it sticks to a reducer with a matching case in its switch) this function - setUser() - to the reducer. That happens in the app. Go to App.js to see a breakdown.
export const mapDispatchToProps = (dispatch) => ({ makeUserList: userArray => dispatch(setUsers(userArray)), makeRoomList: roomArray => dispatch(setRooms(roomArray)), makeBookingsList: bookingArray => dispatch(setBookings(bookingArray)) //notice we're CALLING setUsers here - that's because the argument is NOT setUsers - it's setUsers(). It's what setUsers resolves to. })
So now we're in what I consider to be step 3 - dispatching the info to the reducer.
There are 4 functions to be aware of here. The first is setUsers(). Remember, this is an action creator that we wrote in the actions folder (in step 2) - it's being imported into this file. It takes some info - THE info from step 1, in this case the fetched user array, and it just makes it into an object with the userArray as well as a 'type', which will communicate with the reducer. Again, it could also be written like this:
function setUsers(userList) { return {type: "SET_USERS", userList: userList} }
The second function is dispatch - dispatch is something that I'm guessing comes from connect, which allows us to do a whole bunch of things. Basically, props.dispatch is available to this component as soon as you put in connect.
As far as we're concerned right now, dispatch is magic. It probably looks something like this:
function dispatch(What-the-actionCreatorFunction-returns) { "This object is what the action creator with THE INFO (step 1) as an argument resolves to. It turned THE INFO into an object with a 'type' that will tell the reducer what to do with that info. Send this object to the global store. Throw it against the wall until if finds a reducer that has its type as a case. It will know what to do with it so that the correct thing goes into the store." }
The third is the function that we're assigning to our new prop, makeUserList. Remember how we can drop functions down through components as props? It feels kind of weird giving App, our top-level component (besides Provider) a prop, but what's essentially happening is that connect is jamming these props into when it gets rendered in index without us having to go back and do it. Anyway, we are assign the prop this function. I'm going to think of it like this:
function makeUserList(userArray) { dispatch(setUsers(userArray)) }
So basically, this function is calling dispatch, which takes in an action creator function's return value as an argument, which in turn takes info as an argument, and it sends that action creator function's return value (an action object) to the global state so that it can find the correct reducer so that the reducer can do the appropriate things to it to get it into the store.
Whew.
Last function: mapDispatchToProps. Basically, it takes the dispatch function as an argument, and then I believe it actually CREATES a prop for the component - again, jams it into where it's rendered - that is set to a function that gives dispatch the two things it needs: The action creator to dispatch, and the info to dispatch it with. We're assuming that in a big app we'll be using dispatch a lot, and it will always be dispatching action creators and info in different combinations. Props is a good way to keep track - we could just assign the dispatch function randomly to our onClicks and other functions, but keeping track of what to dispatch, with what info, and when, sounds like a pain. It's probably easier to just assign each permutation its own prop.
Ok cool...but where does it go in the store?? Check what I think of as your global store, which is the argument that the combineReducers function takes in your rootReducer. Is there a key-value pair that goes with the info you're putting there? In this case, it's probably "user", which corresponds to an array of users that check into the hotel (THE INFO, step 1). If there isn't something that fits there, add it to the store, and then give it a reducer. If something is there, add something to its reducer that will set that item's value to what it needs to be. So now, to step 4 - making or updating a reducer. (This could also be step 1 - it's kind of weird to write what you're doing with the info last. But it's just as weird to spin up an action when you don't have the thing you're doing it to, so I choose to do this last. This whole thing is recursive and I hate it). Proceed to the user reducer for step 4.
export const mapStateToProps = state => ({ userList: state.users, infoList: state.infos, itemList: state.items })
'state' is probs another thing we get from connect. It refers to the global state, or the store. mapStateToProps takes the whole global store, or the argument in rootReducer, as an argument, and sets something in to to a local prop that is created in the return of this function. So here, now that we've created a 'users' key in the global store and set it to the list we fetched, we're also setting it to our local props here. There isn't really a good reason to do this here, but it's helpful to see.
btw, connect is the magical function that allows us to do all this.
make sure to install: npm i redux react-redux redux-devtools-extension -S
you need the function 'connect' from react-redux to allow this App class to dispatch the action creator function (setUser()) to the reducer.
export default connect(mapStateToProps, mapDispatchToProps)(App)
DISPATCH GOES SECOND!!
These are your imports in that component:
import React, { Component } from 'react'; import { Route } from 'react-router-dom' import { LoginPage } from '../LoginPage/LoginPage'; import CustomerPage from '../../components/CustomerPage/CustomerPage'; import ManagerPage from '../../components/ManagerPage/ManagerPage'; import { getInfo } from '../../utils/apiCalls'; import { setUsers } from '../../actions/index'; import { setInfos } from '../../actions/index'; import { setItems } from '../../actions/index'; import { connect } from 'react-redux';