My React Learning Note (4)

Firebase ,HOC, Thunk, Sagas

My React Learning Note (4)

Topic: Firebase ,HOC, Thunk, Sagas

Styled-Components

1
2
3
4
5
6
7
const Text = styled.div`
color: red;
font-size: 28px;
border: ${({isActive}) => isActive? "1px solid black": "3px dotted green"}
`

<Text isActive= {tru}>Whatever</Text>

关于 Styled-Components 如果需要自己再学习吧

Firebase

What we did during this session (Forgot to write some codes for this part):

  1. Move Shop Data to Firebase with utility function

  2. Change App.js to a class based component which can fetch all products data from firebase for later use

  3. Create a shop action that updates local state about collections

    all products info are in this.state.shop.collections

  4. Call this shopaction in componentDidMount of shop.component

    Whenever mount the component, we use onSnapshot to listen for snapshot changes or first loading

WithSpinner

WithSpinner is a HOC which wraps a component and return a new component!

Please see the code regarding this point

or this link: HOC EXPLAIN

Observable

auth.onAuthStateChanged(nextValue) is a stream observer. This is always listening to auth state

Every time a user’s auth state is changed, currentUser locally is reset

1
this.unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => ...)

unsubscribeFromAuth is returned by Firebase

Redux Thunk

We need to load resources on pages that need it rather than loading it at once. We can put all loading process in to Redux.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const shopReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ShopActionTypes.FETCH_COLLECTIONS_START:
return {
...state,
isFetching: true,
};
case ShopActionTypes.FETCH_COLLECTIONS_SUCCESS:
return {
...state,
isFetching: false,
collections: action.payload,
};
case ShopActionTypes.FETCH_COLLECTIONS_FAILURE:
return {
...state,
isFetching: false,
errorMessage: action.payload,
};
default:
return state;
}
};

Then we can dispatch in actions (because of Thunk)

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
export const fetchCollectionsStart = () => ({
type: ShopActionTypes.FETCH_COLLECTIONS_START,
});

export const fetchCollectionsSuccess = (collectionsMap) => ({
type: ShopActionTypes.FETCH_COLLECTIONS_SUCCESS,
payload: collectionsMap,
});

export const fetchCollectionsFailure = (errorMessage) => ({
type: ShopActionTypes.FETCH_COLLECTIONS_FAILURE,
payload: errorMessage,
});

export const fetchCollectionsStartAsync = () => {
return (dispatch) => {
const collectionRef = firestore.collection("collections");
// By doing so, we make sure that : state.shop.isLoading = true;
dispatch(fetchCollectionsStart());

// Use promise to fetch collections
collectionRef
.get()
.then((snapshot) => {
const collectionsMap = convertCollectionsSnapshotToMap(snapshot);
dispatch(fetchCollectionsSuccess(collectionsMap));
})
.catch((error) => dispatch(fetchCollectionsFailure(error.message)));
};
};

我们想 separate shop.componentisLoading 的逻辑,可以在 collection-overviewcollection-item 外层加一个 container . container 再从 redux 拿 isLoading state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectIsCollectionFetching } from "../../redux/shop/shop.selectors";
import WithSpinner from "../with-spinner/with-spinner.component";
import CollectionOverview from "./collection-overview.component";
import { compose } from "redux";

// Pass a loading to the WithSpinner component
// No idea why not just use in the WithSpinner component
const mapStateToProps = createStructuredSelector({
isLoading: selectIsCollectionFetching,
});

// right to left
const CollectionOverviewContainer = compose(
connect(mapStateToProps),
WithSpinner
)(CollectionOverview);

//const CollectionOverviewContainer = connect(mapStateToProps)(WithSpinner(CollectionOverviewContainer));
export default CollectionOverviewContainer;

Redux Sagas (Generator)

A function will run under a certain condition,

Impure function: a function will return different outputs even when the input parameter is same.

Move all these impure function to React Sagas, so that Sagas handle the asynchronous calls.

Generator

1
2
3
4
5
6
7
8
9
10
function* gen() {
console.log("a");
console.log("b");
}
const g = gen();
g;
//gen {<suspended>}
g.next();
//a
//b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* gen(i) {
yield i;
yield i + 10;
}

const g = gen(5);
const gObj = g.next();
gObj;
// {value: 5, done : false}
const jObj = g.next();
jObj;
// {value: 15, done: false}
g.next();
//{value: undefined, done: false}

This allows to return

1
2
3
4
5
function* gen(i) {
yield i;
yield i + 10;
return 25;
}

We use yield in Saga for easily cancelling the request during process!

To use sagas, name a function in Sagas with the same name as the redux action call!

Sagas will listen to all same name actions or prepend with on(e.g. onGoogleSignInStart in the same folder

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
import { takeEvery, call, put } from "redux-saga/effects";

import {
firestore,
convertCollectionsSnapshotToMap,
} from "../../firebase/firebase.utils";
import {
fetchCollectionsSuccess,
fetchCollectionsFailure,
} from "./shop.actions";

import ShopActionTypes from "./shop.types";

export function* fetchCollectionsAsync() {
yield console.log("fetchCollectionsAsync in Saga is called");

try {
const collectionRef = firestore.collection("collections");
// similar to async await

const snapshot = yield collectionRef.get();

// call a function that returns a promise
const collectionsMap = yield call(
convertCollectionsSnapshotToMap,
snapshot
);

// Put -> dispatch an action
// if success, we dispatch fetchCollectionsSuccess, store the collectionsMap in the redux store
yield put(fetchCollectionsSuccess(collectionsMap));
} catch (error) {
yield put(fetchCollectionsFailure(error.message));
}
}

// same name to fetchCollectionsStart in shop.actions.js
export function* fetchCollectionsStart() {
yield takeEvery(
ShopActionTypes.FETCH_COLLECTIONS_START,
fetchCollectionsAsync
);
}

Why Sagas

https://stackoverflow.com/questions/65271387/when-to-use-redux-saga-and-when-redux-thunk

In my opinion, redux-saga shines if you need to manage the interactions of long-running tasks. For example, I worked on software for set top boxes which needed to manage changing channels. There’s several asynchronous things that need to be done in sequence to start up playback, and then during playback extra things need to be done periodically. Additionally, every step needs to be cancellable in case the user changes channels. Something like that is easy to do with sagas (assuming you’re used to working with them), but comparatively hard with un-cancellable promises.

If you just need to do fairly simple stuff like “when they click a button, download some data”, then both of them do the job just fine. If your cases are fairly simple, i would lean towards redux-thunk because it’s learning curve is easier than redux-saga. Promises are common in the industry, and so many developers are already used to them; while sagas are a fairly specialized piece of code.

Don’t worry about difference in performance. In both cases, there’s only a very small amount of time spent running the library’s code. The bulk of the time will be spent running your code, and while the style of your code will be quite a bit different, the number of things you need to calculate to do a certain job will be very similar.

take(), takeEvery(), takeLatest()

https://github.com/ZhangMYihua/redux-saga-take-takelatest-takeevery

take(): take only execute one time

takeEvery(): Every action is independent of each other, kick off a new action!

taleEvery is a async way of take in a while loop

1
2
3
4
5
6
7
8
9
import { takeLatest, delay, put } from "redux-saga/effects";

export function* onIncrement() {
yield console.log("I am incremented");
}

export function* incrementSaga() {
yield takeEvery("INCREMENT", onIncrement);
}

takeLatest(): debounce and only wants the latest action to be performed if one is blocked

1
2
3
4
5
6
7
8
9
10
11
import { takeLatest, delay, put } from "redux-saga/effects";

export function* onIncrement() {
yield console.log("I am incremented");
yield delay(3000);
yield put({ type: "INCREMENT_FROM_SAGA" });
}

export function* incrementSaga() {
yield takeLatest("INCREMENT", onIncrement);
}

Overall review

image-20211120200047733

Communication between different action!

With Sagas, we can listen to an action from another component in another component Saga! For example, clear all cart items when a user logout success

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { all, call, put, takeLatest } from "redux-saga/effects";
import { UserActionTypes } from "../user/user.types";
import { clearCart } from "./cart.actions";

export function* clearCartOnSignOut() {
yield put(clearCart());
}

export function* onSignOutSuccess() {
yield takeLatest(UserActionTypes.SIGN_OUT_SUCCESS, clearCartOnSignOut);
}

export function* cartSagas() {
yield all([call(onSignOutSuccess)]);
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!