FCC: Redux

过年之前看了FCC上的Redux,做了习题。

Redux

Create a redux state

  • Redux is a state management framework that can be used with a number of different web technologies, including React.

  • In Redux, there is a single state object that’s responsible for the entire state of your application. This means if you had a React app with ten components, and each component had its own local state, the entire state of your app would be defined by a single state object housed in the Redux store

  • the Redux store is the single source of truth when it comes to application state.

  • This also means that any time any piece of your app wants to update state, it must do so through the Redux store.

  • The Redux storeis an object which holds and manages application state. There is a method called createStore()on the Redux object, which you use to create the Redux store

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const reducer = (state = 5) => {
    return state;
    }

    // Redux methods are available from a Redux object
    // For example: Redux.createStore()
    // Define the store here:

    const store = Redux.createStore(reducer)

Get state from the redux store

  • The Redux store object provides several methods that allow you to interact with it. For example, you can retrieve the current stateheld in the Redux store object with the getState()method.

    1
    2
    3
    4
    5
    6
    const store = Redux.createStore(
    (state = 5) => state
    );

    // change code below this line
    const currentState = store.getState()

Define a redux action

  • In Redux, all state updates are triggered by dispatching actions

  • An action is simply a JavaScript object that contains information about an action event that has occurred. The Redux store receives these action objects, then updates its state accordingly

    1
    2
    // Define an action here:
    const action = {type: 'LOGIN'}

Define an action creator

  • An action creator is simply a JavaScript function that returns an action. In other words, action creators create objects that represent action events.

    1
    2
    3
    4
    5
    const action = {
    type: 'LOGIN'
    }
    // Define an action creator here:
    const actionCreator = () => action

Dispatch an action event

  • dispatchmethod is what you use to dispatch actions to the Redux store

  • Calling store.dispatch()and passing the value returned from an action creator sends an action back to the store.

  • the following lines are equivalent, and both dispatch the action of type LOGIN

    1
    2
    store.dispatch(actionCreator());
    store.dispatch({ type: 'LOGIN' });
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const store = Redux.createStore(
    (state = {login: false}) => state
    );

    const loginAction = () => {
    return {
    type: 'LOGIN'
    }
    };

    // Dispatch the action here:
    store.dispatch(loginAction())

Handle an action in the store

  • After an action is created and dispatched, the Redux store needs to know how to respond to that action. This is the job of a reducerfunction

  • A reducertakes stateand actionas arguments, and it always returns a new state

  • It is important to see that this is the only role of the reducer. It has no side effects — it never calls an API endpoint and it never has any hidden surprises. The reducer is simply a pure function that takes state and action, then returns new state.

  • Another key principle in Redux is that stateis read-only. In other words, the reducerfunction must always return a new copy of stateand never modify state directly.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const defaultState = {
    login: false
    };

    const reducer = (state = defaultState, action) => {
    // change code below this line
    if(action.type === 'LOGIN') {
    return { login: true }
    } else {
    return defaultState
    }
    // change code above this line
    };

    const store = Redux.createStore(reducer);

    const loginAction = () => {
    return {
    type: 'LOGIN'
    }
    };

Use a switch statement to handle multiple actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const defaultState = {
authenticated: false
};

const authReducer = (state = defaultState, action) => {
// change code below this line
switch(action.type) {
case 'LOGIN':
return { authenticated: true}
break;
case 'LOGOUT':
return { authenticated: false }
break;
default:
return defaultState
}
// change code above this line
};

const store = Redux.createStore(authReducer);

const loginUser = () => {
return {
type: 'LOGIN'
}
};

const logoutUser = () => {
return {
type: 'LOGOUT'
}
};

User const for action types

  • A common practice when working with Redux is to assign action types as read-only constants, then reference these constants wherever they are used

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // change code below this line
    const LOGIN = 'LOGIN'
    const LOGOUT = 'LOGOUT'
    // change code above this line

    const defaultState = {
    authenticated: false
    };

    const authReducer = (state = defaultState, action) => {

    switch (action.type) {

    case LOGIN:
    return {
    authenticated: true
    }

    case LOGOUT:
    return {
    authenticated: false
    }

    default:
    return state;

    }

    };

    const store = Redux.createStore(authReducer);

    const loginUser = () => {
    return {
    type: LOGIN
    }
    };

    const logoutUser = () => {
    return {
    type: LOGOUT
    }
    };

Register a store listener

  • Another method you have access to on the Redux storeobject is store.subscribe(). This allows you to subscribe listener functions to the store, which are called whenever an action is dispatched against the store

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    const ADD = 'ADD';

    const reducer = (state = 0, action) => {
    switch(action.type) {
    case ADD:
    return state + 1;
    default:
    return state;
    }
    };

    const store = Redux.createStore(reducer);

    // global count variable:
    let count = 0;

    // change code below this line
    store.subscribe(() => count ++ )
    // change code above this line

    store.dispatch({type: ADD});
    console.log(count);
    store.dispatch({type: ADD});
    console.log(count);
    store.dispatch({type: ADD});
    console.log(count);

Combine multiple reducers

  • When the state of your app begins to grow more complex, it may be tempting to divide state into multiple pieces. Instead, remember the first principle of Redux: all app state is held in a single state object in the store.

  • ou define multiple reducers to handle different pieces of your application’s state, then compose these reducers together into one root reducer. The root reducer is then passed into the Redux createStore()method.

  • In order to let us combine multiple reducers together, Redux provides the combineReducers()method. This method accepts an object as an argument in which you define properties which associate keys to specific reducer functions

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    const INCREMENT = 'INCREMENT';
    const DECREMENT = 'DECREMENT';

    const counterReducer = (state = 0, action) => {
    switch(action.type) {
    case INCREMENT:
    return state + 1;
    case DECREMENT:
    return state - 1;
    default:
    return state;
    }
    };

    const LOGIN = 'LOGIN';
    const LOGOUT = 'LOGOUT';

    const authReducer = (state = {authenticated: false}, action) => {
    switch(action.type) {
    case LOGIN:
    return {
    authenticated: true
    }
    case LOGOUT:
    return {
    authenticated: false
    }
    default:
    return state;
    }
    };

    const rootReducer = Redux.combineReducers({
    count: counterReducer,
    auth: authReducer,
    })// define the root reducer here

    const store = Redux.createStore(rootReducer);

Send action data to the store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const ADD_NOTE = 'ADD_NOTE';

const notesReducer = (state = 'Initial State', action) => {
switch(action.type) {
// change code below this line
case ADD_NOTE:
return state = action.text
// change code above this line
default:
return state;
}
};

const addNoteText = (note) => {
// change code below this line
return {
type: ADD_NOTE,
text: note,
}
// change code above this line
};

const store = Redux.createStore(notesReducer);

console.log(store.getState());
store.dispatch(addNoteText('Hello!'));
console.log(store.getState());

Use middleware to handle asynchronous actions

  • At some point you’ll need to call asynchronous endpoints in your Redux app, so how do you handle these types of requests? Redux provides middleware designed specifically for this purpose, called Redux Thunk middleware

  • To include Redux Thunk middleware, you pass it as an argument to Redux.applyMiddleware(). This statement is then provided as a second optional parameter to the createStore()function.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    const REQUESTING_DATA = 'REQUESTING_DATA'
    const RECEIVED_DATA = 'RECEIVED_DATA'

    const requestingData = () => { return {type: REQUESTING_DATA} }
    const receivedData = (data) => { return {type: RECEIVED_DATA, users: data.users} }

    const handleAsync = () => {
    return function(dispatch) {
    // dispatch request action here
    store.dispatch(requestingData())
    setTimeout(function() {
    let data = {
    users: ['Jeff', 'William', 'Alice']
    }
    // dispatch received data action here
    store.dispatch(receivedData(data))
    }, 2500);
    }
    };

    const defaultState = {
    fetching: false,
    users: []
    };

    const asyncDataReducer = (state = defaultState, action) => {
    switch(action.type) {
    case REQUESTING_DATA:
    return {
    fetching: true,
    users: []
    }
    case RECEIVED_DATA:
    return {
    fetching: false,
    users: action.users
    }
    default:
    return state;
    }
    };

    const store = Redux.createStore(
    asyncDataReducer,
    Redux.applyMiddleware(ReduxThunk.default)
    );

Write a counter with redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const INCREMENT = 'INCREMENT'; // define a constant for increment action types
const DECREMENT = 'DECREMENT'; // define a constant for decrement action types

const counterReducer = (state = 0, action) => {
switch(action.type) {
case INCREMENT:
return ++ state
case DECREMENT:
return -- state
default:
return state
}
}; // define the counter reducer which will increment or decrement the state based on the action it receives

const incAction = () => {
return {
type: INCREMENT
}
}; // define an action creator for incrementing

const decAction = () => {
return {
type: DECREMENT
}
}; // define an action creator for decrementing

const store = Redux.createStore(counterReducer); // define the Redux store here, passing in your reducers

Never mutate state

  • Immutable state means that you never modify state directly, instead, you return a new copy of state.

  • If you took a snapshot of the state of a Redux app over time, you would see something like state 1, state 2, state 3,state 4, ...and so on where each state may be similar to the last, but each is a distinct piece of data.

  • This immutability, in fact, is what provides such features as time-travel debugging that you may have heard about.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const ADD_TO_DO = 'ADD_TO_DO';

    // A list of strings representing tasks to do:
    const todos = [
    'Go to the store',
    'Clean the house',
    'Cook dinner',
    'Learn to code',
    ];

    const immutableReducer = (state = todos, action) => {
    switch(action.type) {
    case ADD_TO_DO:
    // don't mutate state here or the tests will fail
    return [
    ...todos,
    action.todo,
    ]
    default:
    return state;
    }
    };

    // an example todo argument would be 'Learn React',
    const addToDo = (todo) => {
    return {
    type: ADD_TO_DO,
    todo
    }
    }

    const store = Redux.createStore(immutableReducer);

Use the spread operator on arrays

  • One solution from ES6 to help enforce state immutability in Redux is the spread operator: ...

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const immutableReducer = (state = ['Do not mutate state!'], action) => {
    switch(action.type) {
    case 'ADD_TO_DO':
    // don't mutate state here or the tests will fail
    return [...state, action.todo]
    default:
    return state;
    }
    };

    const addToDo = (todo) => {
    return {
    type: 'ADD_TO_DO',
    todo
    }
    }

    const store = Redux.createStore(immutableReducer);

Remove an item from an array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const immutableReducer = (state = [0,1,2,3,4,5], action) => {
switch(action.type) {
case 'REMOVE_ITEM':
// don't mutate state here or the tests will fail
const result = [...state]
result.splice(action.index,1)
return result
default:
return state;
}
};

const removeItem = (index) => {
return {
type: 'REMOVE_ITEM',
index
}
}

const store = Redux.createStore(immutableReducer);

Copy an object with Object.assign

  • Object.assign()takes a target object and source objects and maps properties from the source objects to the target object

  • Any matching properties are overwritten by properties in the source objects

  • This behavior is commonly used to make shallow copies of objects by passing an empty object as the first argument followed by the object(s) you want to copy.

  • const newObject = Object.assign({}, obj1, obj2)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    const defaultState = {
    user: 'CamperBot',
    status: 'offline',
    friends: '732,982',
    community: 'freeCodeCamp'
    };

    const immutableReducer = (state = defaultState, action) => {
    switch(action.type) {
    case 'ONLINE':
    // don't mutate state here or the tests will fail
    return Object.assign({}, defaultState, {status: 'online'})
    return
    default:
    return state;
    }
    };

    const wakeUp = () => {
    return {
    type: 'ONLINE'
    }
    };

    const store = Redux.createStore(immutableReducer);