Welcome to continue reading the Taro Small program Development large-scale Practical series, previously on:

  • Familiar React, familiar Hooks: We implemented a very simple prototype for adding posts using React and Hooks
  • Multi-page hops and the Taro UI Component Library: We implemented multi-page hops with the built-in Taro routing function, and upgraded the application interface with the Taro UI component library
  • Realize wechat and Alipay multi-terminal login: realize wechat, Alipay and ordinary login and logout login

If you follow this, you will find that state management and data flow are becoming more and more bloated, and component state updates are very complicated. In this article, we will start refactoring with Redux. Since this refactoring involves a lot of file changes, we will cover this step with Redux in two articles, this is the first.

If you’re not familiar with Redux, we recommend reading our Redux Tutorial series:

  • Redux Redux Redux Redux Redux Redux Redux Redux Redux Redux
  • Redux: Strike while the iron is hot and completely refactor
  • Redux Education Package Association (3) : To perform their respective duties and regain the original purpose

If you want to start directly from this step, run the following command:

git clone -b redux-start https://github.com/tuture-dev/ultra-club.git
cd ultra-club
Copy the code

The source code for this article is available on Github. If you think we did a good job, please give ❤️ a thumbs up +Github repository + star ❤️

A combination of double swords

The SRC /pages/mine/mine.jsx file is the top component of many states. Such as our common SRC/components/login button LoginButton/index JSX components and our SRC/components/Footer/index. The JSX components, IsOpened is operated in LoginButton, which then affects the FloatLayout popover in Footer component. In this case, multiple sub-components communicate with each other. The way we save state to the common parent is called “state promotion” in React.

However, as the number of states increases, so does the number of states promoted by the state, resulting in bloated parent components that store these states. In addition, each state change needs to affect many intermediate components, bringing great performance overhead. Such a state management problem is usually handled by Redux, a special state management container. React instead focuses on rendering the user interface.

Redux not only ensures the predictability of state, but also ensures that state changes are only relevant to the corresponding component and do not affect irrelevant components. For a detailed tutorial on Redux, see the Redux tutorial series.

In this section, we will combine React Hooks and Redux to refactor our state management.

Install dependencies

First let’s install the necessary dependencies to use Redux:

$ yarn add redux @tarojs/redux @tarojs/redux-h5  redux-logger
Or use NPM
$ npm install --save redux @tarojs/redux @tarojs/redux-h5 redux-logger
Copy the code

In addition to the familiar Redux dependencies and the middleware redux-Logger used to print actions, there are two additional packages. This is because in Taro, Redux’s react-redux library has been replaced with @tarojs/ Redux and @tarojs/redux-h5, the former used in applets and the latter used in H5 pages. Taro encapsulates the original React-Redux and provides a package that is almost identical to the React-Redux API to give developers a better development experience.

Create a Redux Store

The three core concepts of Redux are: Store, Action and Reducers:

  • Store: Holds global state and is known as “the only source of truth for data”.
  • Action: Initiates the Action to change the saved state in the Store. It is the only means to change the state.
  • Reducers: A series of pure functions used to modify the state in the Store in response to actions.

Ok, now that we’ve reviewed the concept of Redux, we’re going to create stores, and Redux best practices recommend that we save stores in the Store folder, we create the Store folder under the SRC folder, And create index.js in it to write our Store:

import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import rootReducer from '.. /reducers'

const middlewares = [createLogger()]

export default function configStore() {
  conststore = createStore(rootReducer, applyMiddleware(... middlewares))return store
}
Copy the code

As you can see, we exported a configureStore function and created and returned a Store. Here we use the Redux-Logger middleware to initiate an Action. Print the state information saved in the Action and its surrounding stores on the console.

Here our createstore receives two parameters: rootReducer and applyMiddleware(… Middlewares).

RootReducer is the reducer of the response actions. Here we export a rootReducer that represents all reducer assemblers, which we will discuss in “Combining users and Post Reducer” later.

For the second argument to createStore, we use applyMiddleware, a utility function provided by Redux, to inject middleware into Redux because it receives arguments such as args1, args2, args3… , argsn), so here we use the array expansion operator… To expand the Middlewares array.

Write the User Reducer

After creating the Store, we went to compile the Reducer. Back to our page logic, we have two tabs at the bottom, and one for the “home page”, an “I”, the “home page” in the main is to show a list of articles and allows you to add articles, etc., in the “I” in the main is to allow the user to log in and show the login information, so overall we there are two types of logic, We named them Post and User, respectively, and we’ll create reducers that handle both types of logic.

The logic form of Reducer is (state, action) => newState, that is to receive the last state and the modified state action, and return the newState after modification, which is a pure function. That means we can’t change the state by mutation.

Recommendation:

newState = { ... state,prop: newValue }
Copy the code

Is not recommended:

state.prop = newValue
Copy the code

The best practice recommended by Redux is to create separate Reducers folders where we save our reducer files individually. We created the reducers folder under the SRC folder, created the user.js file in it, and added the corresponding content of our User Reducer as follows:

import { SET_LOGIN_INFO, SET_IS_OPENED } from '.. /constants/'

const INITIAL_STATE = {
  avatar: ' '.nickName: ' '.isOpened: false,}export default function user(state = INITIAL_STATE, action) {
  switch (action.type) {
    case SET_IS_OPENED: {
      const { isOpened } = action.payload

      return { ...state, isOpened }
    }

    case SET_LOGIN_INFO: {
      const { avatar, nickName } = action.payload

      return { ...state, nickName, avatar }
    }

    default:
      return state
  }
}
Copy the code

We declared the initial state INITIAL_STATE of the User Reducer in user.js and assigned it to the default value of the user function state, which receives the actions to be responded to, Inside the user function is a switch statement that evaluates action. Type and executes the corresponding logic. Here we have two main types: SET_IS_OPENED is used to change the isOpened property, SET_LOGIN_INFO is used to change the Avatar and nickName properties. If no action. Type value is matched in the switch statement, it returns the original state.

prompt

According to recent Redux practice, the SET_IS_OPENED and SET_LOGIN_INFO constants here are generally saved in the Constants folder, which we will create immediately. The purpose of using constants here rather than hardcoding strings directly is for the maintainability of the code.

Next we will create the constants we will use in SRC /reducer/user.js. We will create the constants folder in SRC and create user.js and add the following:

export const SET_IS_OPENED = 'MODIFY_IS_OPENED'
export const SET_LOGIN_INFO = 'SET_LOGIN_INFO'
Copy the code

Write a Post Reducer

In response to the state modification of the POST logic, we created post.js under SRC /reducers and wrote the following in it:

import { SET_POSTS, SET_POST_FORM_IS_OPENED } from '.. /constants/'

import avatar from '.. /images/avatar.png'

const INITIAL_STATE = {
  posts: [{title: 'Telo Altman'.content: "Tiro is the only biological son of the Father and mother of Otter.".user: {
        nickName: 'Tufinch sauce',
        avatar,
      },
    },
  ],
  isOpened: false,}export default function post(state = INITIAL_STATE, action) {
  switch (action.type) {
    case SET_POSTS: {
      const { post } = action.payload
      return { ...state, posts: state.posts.concat(post) }
    }

    case SET_POST_FORM_IS_OPENED: {
      const { isOpened } = action.payload

      return { ...state, isOpened }
    }

    default:
      return state
  }
}
Copy the code

It can be seen that the form of Post Reducer is similar to that of User Reducer. We extracted state posts and isOpened that needed to be shared in multiple components before and saved them in the state of Post. The Post function here mainly responds to the SET_POSTS logic. It is used to add new post-to-posts states and SET_POST_FORM_IS_OPENED logic. The user sets isOpened state.

Next we create the constants we will use in SRC /reducer/post.js, we create a user.js file under SRC /constants and add the following:

export const SET_POSTS = 'SET_POSTS'
export const SET_POST_FORM_IS_OPENED = 'SET_POST_FORM_IS_OPENED'
Copy the code

Those of you who are sharp-eyed may have noticed that we import the constants we need to use in SRC /reducers/user.js and SRC /reducers/post.js from.. /constants, that’s because we created an index.js file under the SRC /constants folder to export all constants in a uniform way, which is also an attempt at code maintainability.

export * from './user'
export * from './post'
Copy the code

Combine the User and Post Reducer

We have previously split the whole global response logic into SRC /reducers/user.js and SRC /reducers/post.js respectively, which enables us to split the response logic into many small function units, greatly increasing the readability and maintainability of the code.

Ultimately, we will combine the split logic into a logical tree and pass it as a parameter to the createStore function.

Redux provides combineReducers for us to combine the split logic. We created an index.js file under the SRC /reducers folder and wrote the following in it:

import { combineReducers } from 'redux'

import user from './user'
import post from './post'

export default combineReducers({
  user,
  post,
})
Copy the code

As you can see, we import user.js and post.js and pass them to the combineReducers function using the object introduction method and export them. CombineReducers combines the logic and exports it to rootReducer as arguments to be used in our SRC /store/index.js createStore function.

The combineReducers function here mainly accomplishes two things:

  • Combine the state from the User Reducer and post Reducer, and merge them into a shape as follows{ user, post }Of the state tree, whereuserProperty to save the state of the user Reducer,postProperty holds the state of the Post Reducer.
  • Distribute actions when in the componentdispatchAn Action,combineReducersThe user Reducer and post Reducer are traversed, and a Reducer is matched to any ReducerswitchStatement, the Action is responded to.

prompt

We’ll explain how to dispatch actions in a component shortly.

Integrate Redux and React

After we wrote the reducers and created the Store, the next step to consider was how to integrate Redux into React. We opened SRC /app.js and made the following changes:

import Taro, { Component } from '@tarojs/taro'
import { Provider } from '@tarojs/redux'

import configStore from './store'
import Index from './pages/index'
import './app.scss'

// ...

const store = configStore()

class App extends Component {
  config = {
    // ...
  }

  render() {
    return (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }
}

Taro.render(<App />, document.getElementById('app'))
Copy the code

As you can see, there are three main changes to the above content:

  • We importconfigureStoreAnd call it to getstore.
  • Next we take the Taro binding library from Redux@tarojs/reduxexportProviderIt provides a bridge between Redux and React.
  • And finally we useProviderWrap our previous root component and willstorePassed in as its property so that subsequent components can be retrievedstoreThe state saved inside.

Action is new from Hooks

With the Store and Reducer in place, and Redux and React integrated, it’s time to experience the power of the Redux state management container, but in order to use the Hooks version of Action, here are the Hooks that will be used.

useDispatch Hooks

This Hooks returns the Dispatch reference to the Redux store. You can use it to dispatch actions.

Finished useDispatch Hooks, we come back to the practice of a wave, first of all we “common login” Redux of the problem, let’s open the SRC/components/LoginButton/index, js, make corresponding change of the content is as follows:

import Taro from '@tarojs/taro'
import { AtButton } from 'taro-ui'
import { useDispatch } from '@tarojs/redux'

import { SET_IS_OPENED } from '.. /.. /constants'

export default function LoginButton(props) {
  const dispatch = useDispatch()

  return (
    <AtButton
      type="primary"
      onClick={()= >Dispatch ({type: SET_IS_OPENED, payload: {isOpened: true}})} > Normal login</AtButton>)}Copy the code

As you can see, there are four main changes:

  • First we start with@tarojs/reduxexportuseDispatchAPI.
  • Next we export from the constant file we defined earlierSET_IS_OPENEDConstants.
  • And then, we were inLoginButtonCalled in a functional componentuseDispatchHooks to return oursdispatchFunction that we can use to dispatch actions to modify the state of the Redux store
  • And finally we are going toAtButtononClickThe received callback function is replaced, and when the button is clicked, we initiate onetypeSET_IS_OPENEDAction, and passed onepayloadParameter, used to Redux the correspondinguserProperties of theisOpenedModified totrue.

Fix the “common login”, we went on to tidy up “WeChat login” logic, open the SRC/components/WeappLoginButton/index. The js file, the file content to make the following changes:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'
import { useDispatch } from '@tarojs/redux'

import './index.scss'
import { SET_LOGIN_INFO } from '.. /.. /constants'

export default function WeappLoginButton(props) {
  const [isLogin, setIsLogin] = useState(false)

  const dispatch = useDispatch()

  async function onGetUserInfo(e) {
    setIsLogin(true)

    const { avatarUrl, nickName } = e.detail.userInfo

    await Taro.setStorage({
      key: 'userInfo'.data: { avatar: avatarUrl, nickName },
    })

    dispatch({
      type: SET_LOGIN_INFO,
      payload: {
        avatar: avatarUrl,
        nickName,
      },
    })

    setIsLogin(false)}// return ...
}
Copy the code

As you can see, the changes above are similar to those made in normal Login:

  • We have deriveduseDispatchhook
  • Derived bySET_LOGIN_INFOconstant
  • We then pass in the parent component that was called earliersetLoginInfoThe method was changed to DispatchtypeSET_LOGIN_INFOAction because of ouravatarnickNameState already instoreIn theuserProperty, so we also need to modify through the Dispatch action, and finally we define the previous in the parent componentTaro.setStorageThe method of setting the cache is moved to the child components to ensure consistent changes to the information.

We finally to get “pay treasure to log in” the story of logic, open the SRC/components/AlipayLoginButton/index, js to make corresponding change the file content is as follows:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'
import { useDispatch } from '@tarojs/redux'

import './index.scss'
import { SET_LOGIN_INFO } from '.. /.. /constants'

export default function AlipayLoginButton(props) {
  const [isLogin, setIsLogin] = useState(false)
  const dispatch = useDispatch()

  async function onGetAuthorize(res) {
    setIsLogin(true)
    try {
      let userInfo = await Taro.getOpenUserInfo()

      userInfo = JSON.parse(userInfo.response).response
      const { avatar, nickName } = userInfo

      await Taro.setStorage({
        key: 'userInfo'.data: { avatar, nickName },
      })

      dispatch({
        type: SET_LOGIN_INFO,
        payload: {
          avatar,
          nickName,
        },
      })
    } catch (err) {
      console.log('onGetAuthorize ERR: ', err)
    }

    setIsLogin(false)}// return ...
}
Copy the code

As you can see, the changes above are almost the same as those in “wechat login”, so we won’t repeat the explanation here 🙂

UseSelector Hooks

With all the way down the students may be a bit understand that we are using Redux code before us, and we are the thinking of refactoring is first from SRC/pages/mine/mine. The JSX in SRC/components/headers/index. The JSX began, Fix the Header. The inside of the JSX all login button, the following should turn to the Header. The last component in the JSX SRC/components/LoggedMine/index. The JSX.

Because we need useSelector Hooks in the LoggedMine component, let’s start with the Hooks.

useSelector Hooks

UseSelector allows you to retrieve data from a Redux Store using a selector function.

The Selector function is roughly equivalent to the mapStateToProps parameter of the connect function. Selector is called every time the component is rendered. UseSelector also subscribes to the Redux Store and is called when the Redux action is dispatched.

But useSelector still has some differences from mapStateToProps:

  • Don’t likemapStateToPropsJust like returning an object, Selector might return any value.
  • When an action Dispatch occurs,useSelectorIt does a shallow comparison of the return values of the selectors before and after, and if they’re different, the component forces an update.
  • Selector is not acceptedownPropsParameters. But a selector can access props passed down from a functional component through a closure.

Good, understand the concept of useSelector, after we come back to the field, open the SRC/components/LoggedMine/index. The JSX file, some content to make the following changes:

import Taro from '@tarojs/taro'
import { View, Image } from '@tarojs/components'
import { useSelector } from '@tarojs/redux'
import { AtAvatar } from 'taro-ui'

import './index.scss'

export default function LoggedMine(props) {
  const nickName = useSelector(state= > state.user.nickName)
  const avatar = useSelector(state= > state.user.avatar)

  function onImageClick() {
    Taro.previewImage({
      urls: [avatar],
    })
  }

  return( <View className="logged-mine"> {avatar ? ( <Image src={avatar} className="mine-avatar" onClick={onImageClick} /> ) : (<AtAvatar size="large" circle text=" small "/>)} <View className=" mine-nickname ">{nickName}</View> </View>)}Copy the code

As you can see, our code above has four major changes:

  • First we start with@tarojs/reduxThe deduceduseSelectorHooks.
  • And then we used it twiceuseSelectorRetrieved from the Redux StorenickNameavatar, they are locatedstate.userUnder the properties.
  • And then we’re going to go frompropsStudent: I got it in therenickNameavatarInstead, we get the state from the Redux Store, and here we get it from the user experiencetaro-uiExport one from theAtAvatarComponents used to display in noavatarIs the default profile picture.
  • Finally, click on the avatar to previewonImageClickIn the method, we use the Redux storeavatar.

It is time to harvest the last wave “leek”, let’s complete the Header/index of js Redux, open the SRC/components/headers/index. The js, a change to the content of them is as follows:

// ...
import { useSelector } from '@tarojs/redux'

// import various components...

export default function Header(props) {
  const nickName = useSelector(state= > state.user.nickName)

  // To construct a Boolean value corresponding to the string, which is used to indicate whether the user is logged in
  constisLogged = !! nickNameconst isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
  const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY

  return (
    <View className="user-box">
      <AtMessage />
      <LoggedMine />{! isLogged && (<View className="login-button-box">
          <LoginButton />
          {isWeapp && <WeappLoginButton />}
          {isAlipay && <AlipayLoginButton />}
        </View>
      )}
    </View>)}Copy the code

As you can see, there are five major changes to the code above:

  • So first we derived ituseSelectorHooks.
  • And then we useuseSelectorTo get what we neednickNameProperty for double-take inversion to a Boolean valueisLogged“Indicates whether to log in.
  • We then take what we got earlier from the parent componentprops.isLoggedProperty is replaced with the new fromisLogged
  • Next, we remove what is no longer needed on the “Normal Login” buttonhandleClickProperties and “wechat Login”, “Alipay login” above are no longer requiredsetLoginInfoProperties.
  • And finally, we get rid ofLoggedMineNo longer needed on the componentuserInfoProperty because we already use it inside the componentuseSelectorHooks are retrieved from inside the component.

summary

In this article, we have covered refactoring the state management of the user logic. Due to space constraints, we have left the Footer part of the user logic to be covered. In the next article, we will first cover refactoring the state management of the Footer component using the Hooks version of Redux. Let’s talk about state management for the POST part of refactoring.

Want to learn more exciting practical skills tutorial? Come and visit the Tooquine community.

The source code for this article is available on Github. If you think we did a good job, please give ❤️ a thumbs up +Github repository + star ❤️