The overall graphic

I have been using UMI for some time. Personally, I understand that UMiJS is a combination of a large number of popular React extension solutions. It has built-in plug-ins such as DVA data stream and React-Router, which are all integrated into UMI for developers to use directly.

Two key points in UMI are routing scheme and DVA data flow. Routing configuration is very elegant, and dVA + ANTD has a very good effect.

Here is a data trend for DVA data flow.

fromaspirantzhangDraw sketches

The core idea of DVA is to separate the page from the data layer, which is called Model and contains Reducer, Effect, and Subscription objects.

Pages and data are associated with connect, which is the same thing if you know About Redux.

It’s 2021, and the community has long since moved away from Connect, but since many companies still use higher-level components to pass state, I’ll write a demo with Connect. Write a separate method for hooks.

Here are some concepts

Reducer

It contains the synchronization code, which is the unified socket that formally sends data to the page. We can only return data to the page at this layer.

Its parameters look like this

type Reducer<S, A> = (state: S, action: A) = > S
Copy the code

State is the original data, which is a generic type. The return value of the Reducer function must be the same as state.

The action is the {type,payload} object

This action is the action with dispatch(Action).

Effect

The contents of this object are asynchronous functions, so if we want to send ajax requests or other asynchronous functions, we’ll put them in this layer.

A closer look at the sketch shows that Effect also extends out a service that houses many asynchronous functions. For example, if a user performs an operation that triggers an asynchronous function, these asynchronous functions can be placed in a service and returned to Effect by call.

Effect must be genarator. Async and await are invalid. But we can use async, await in a service.

Another point to note is that all data must be sent to the Reducer page through the reducer, so the data obtained asynchronously needs to be sent to the Reducer through the put call.

So where are the call and put functions? It exists in the function defined by Effect. Let’s look at the parameters of an Effect function.

*(action, effects) = > void
Copy the code

Call and put are in Effects.

Subscription

The business scenario I often use for this is to trigger an asynchronous function in Effect to fetch data through a route change.

Let’s look at its usage parameters

({ dispatch, history }, done) => unlistenFunction
Copy the code

Here is a Model use case

Take a look at some code:

  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) = > {
        if (pathname === '/') {
          dispatch({
            type: 'query'}); }}); }},Copy the code

In the use case, you call the Listen method in History directly to listen for changes in the path of the page, or dispatch it if it is /.

The history is actually an object of the History library, which acts as a wrapper around the History native API. See the MDN – history

practice

1. Configure routes

With these basic concepts in mind, we can simulate real development by writing a simple example.

We need to install UMi3 first, please check the official website for installation

When you’re done installing, your directory structure should look like this

... ├ ─ ─ editorconfig ├ ─ ─ gitignore ├ ─ ─ the prettierignore ├ ─ ─ the prettierrc ├ ─ ─ the umirc. Ts ├ ─ ─ the README. Md ├ ─ ─ the mock │ └ ─ ─ . Gitkeep ├ ─ ─ package. Json ├ ─ ─ the SRC │ └ ─ ─ pages │ ├ ─ ─ but less │ └ ─ ─ index. The TSX ├ ─ ─ tsconfig. Json └ ─ ─ typings. Which sCopy the code

We write all our code in Pages, which is associated with routing. There are two types of UMI routes: contract route and configuration route. We use configuration routing directly.

$ mkdir config && cd config
$ touch config.ts routes.ts
Copy the code

By doing this, we created these structures separately

├── bass exercises ─ bass exercises ─ routesCopy the code

Then write the following:

// config/config.ts

import { defineConfig } from 'umi';
import routes from './routes';

export default defineConfig({
  routes: routes,
});
Copy the code
// config/routes.ts
export default[{exact: true.path: '/'.component: './index' },
  { exact: true.path: '/users'.component: './users/users'},];Copy the code

Yarn start accesses /users to see if it takes effect.

If it is blank, then congratulations, this is normal, we haven’t written the users file yet.

As an explanation, the above operation is to write the Route. If you are familiar with react-Route, you should be able to read the properties.

Configuration routing is very convenient. It indexes the SRC /pages directory by default, so we just need to write./index under Component instead of./ SRC /page/index.

2. Create a directory

Now we want to create a Users directory to match our routing, we need to create it like this

cd src/pages && mkdir users && cd users
touch Users.tsx model.ts service.ts
Copy the code

This gives you the following directory structure

. ├ ─ ─ model. Ts ├ ─ ─ service. The ts └ ─ ─ the users. The TSXCopy the code

Then write user.tsx

import React from 'react';

const Users = () = > {
  return <div>users</div>;
};

export default Users;
Copy the code

If you haven’t deleted.umirc.ts at this point, there is a high probability that it will fail.

Umirc. Ts has a higher priority than config, which is usually used for very simple projects. In real development, it’s important to write code with minimal rules, and configuration files are no exception, so I recommend splitting everything out and deleting.umirc.ts.

Then run YARN Start to access/Users. You should see the letters Users appear on the page.

3. Define the model

The model consists of the following

const model = {
  namespace: ' '.state: {},
  reducers: {},
  effects: {},
  subscriptions: {}};export default model;
Copy the code

Namespace is the namespace of Model and is also an attribute of State

State is the initial repository

Reducers are objects that save functions for processing synchronous data and return data to view layer through reducer, which is a unified data transmission and binding interface

Effects are used to hold objects that handle asynchronous functions, and the data that passes through effcets needs to be put to reducers to be returned to the data layer

Subscriptions are used to subscribe to data sources.

It doesn’t matter if you don’t understand it, let’s write a piece of data with the overall diagram. For example, I now need to put data in the Users page. Define a function directly in reducers and return it.

  reducers: {
    getList() {
      return 123; }},Copy the code

When a page enters users, it needs to subscribe to a data, so we write this in Subscriptions

  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) = > {
        if (pathname === '/users') {
          dispatch({
            type: 'getList'}); }}); }},Copy the code

History is used to monitor the path. If the path is /users, dispatch an action. The type in the action is the function name in the Reducer, which indicates that the reducer function is triggered.

And finally, remember to write the namespace property, so I’m going to write users here, which is very important, and we’ll use it next.

4. Connect the data layer to the page layer

Let’s use CONNECT to connect the data layer to the page layer.

// Users.tsx
import React from 'react';
import { connect } from 'umi';

const mapStateToProps = (state) = > {
  const { users } = state;
  return { users };
};
export default connect(mapStateToProps)(Users);
Copy the code

As with Redux, we need to write a mapStateToProps function, pass it to connect, return a function, and pass it to the Users component, which will return a new component.

The Users component can then get the data through props.

const Users = (props) = > {
  return <div>users{props.users}</div>;
};
Copy the code

What is props. Users here? It is a namespace write property. The namespace allows you to customize the key in the state and obtain the data in the key.

If it’s okay, it should be on your page

users123
Copy the code

5. Asynchronous operation Effects

In a real web page, usually we jump to a route, we need to get data asynchronously and then render the page, so we need to use Effects.

Since the data is too simple, we’d better use the Table component in Ant-Design to make the Users page look better.

So let’s just let Effects work with us.

(1) First, we define a data

// model.ts
      const data = [
        {
          key: '1'.name: 'John Brown'.age: 32.address: 'New York No. 1 Lake Park'.tags: ['nice'.'developer'],}, {key: '2'.name: 'Jim Green'.age: 42.address: 'London No. 1 Lake Park'.tags: ['loser'],},];Copy the code

(2) Modify subscription subscriptions

We need to change the Dispatch from Subscriptions to this

          dispatch({
            type: 'asyncGetData'});Copy the code

So I’m going to change the name of the function defined in Effects.

This operation means that the asyncGetData function is executed when the route is /users.

(3) Write genarator function in Effects

  effects: {
    *asyncGetData(action, effects) {
      const listData = yield Promise.resolve(data);
      // Push the data to the reducers with effects.put and trigger the gitList function
      yield effects.put({ type: 'getList'.payload: listData }); }},Copy the code

Put receives an action as a parameter. We directly pass the defined data to the getList method in the Reducers through the payload in the action, and the getList transmits the payload to the page.

Since Reducers was the only place to connect to the front end page, we had to.

(4) The Reducer function receives and sends to the page layer

  reducers: {
    getList(state, action) {
      return { listData: action.payload };
    },
Copy the code

(5) Finally modify the page in users.tsx

Here is a copy of the component code from table-ant-Design.

const Users = (props) = > {
  const columns = [
    {
      title: 'Name'.dataIndex: 'name'.key: 'name'.render: (text) = > <a>{text}</a>}, {title: 'Age'.dataIndex: 'age'.key: 'age'}, {title: 'Address'.dataIndex: 'address'.key: 'address'}, {title: 'Tags'.key: 'tags'.dataIndex: 'tags'.render: (tags) = > (
        <>
          {tags.map((tag) => {
            let color = tag.length > 5 ? 'geekblue' : 'green';
            if (tag === 'loser') {
              color = 'volcano';
            }
            return (
              <Tag color={color} key={tag}>
                {tag.toUpperCase()}
              </Tag>
            );
          })}
        </>),}, {title: 'Action'.key: 'action'.render: (text, record) = > (
        <Space size="middle">
          <a>Delete</a>
        </Space>),},];// Focus here
  return <Table columns={columns} dataSource={props.users.listData} />;
};
Copy the code

We have crossed the data to the page via props. Users. ListData.

Pay attention to the point

There is a small problem that needs attention. Why did I return an object in the Reducer?

    getList(state, action) {
      return { listData: action.payload };
    },
Copy the code

In the third step of defining the model, we returned 123 directly, and then got the passed data directly through props. Users.

But in this case, it would be logical for me to return the data and get the data directly from props. Users.

Because I ran into a bug here, I can see the state data through redux’s Chrome plugin.

There is indeed an array in the Users property. On the page, however, the users passed to props did not get this data.

So the solution here is to wrap it around an object when you return.

6.service.ts

The service file is mainly used for asynchronous functions, where we can write the logic of asynchronous functions.

Resolve is used to return data. In practice, ajax is used to send a request to return data, so we define a function in the service to get data asynchronously.

Since steps 1-5 used mock data, I used interface data here, so I created a new route.

├── bass Exercises ── ├── bass exercisesCopy the code

If it is convenient, write the new routing component and model data again in steps 1-5 to make it easier to understand.

I omit the process of redoing the component below, because the programs are similar, so I write the service interaction directly.

$ yarn add axios
Copy the code
// service.ts
import axios from 'axios';
export async function getRemote(params) {
  const { data } = await axios
    .get('http://public-api-v1.aspirantzhang.com/users')
    .then(null.(error) = > {
      throw new Error(error);
    });
  return data;
}
Copy the code

Asynchronous functions in the service file support async and await. The above interface is also provided by AspirantZhang and will not have cross-domain issues.

Finally, we need to call in the asynchronous function from the Service

// login/model.ts
  effects: {*asyncGetData(action, effects) {
      //const listData = yield Promise.resolve(data);
      // Get the data from the actual interface
      const {data} = yield effects.call(getRemote);
      // Push the data to reducers with effects.put
      yield effects.put({ type: 'getList'.payload: data }); }},Copy the code

If you’ve written it yourself and still have problems, check out my Github for a comparison

Page interaction with the repository

Let’s try modifying the data in the LOGIN component again. Start by creating a new folder and file

$ mkdir components && touch UseModal.tsx
Copy the code

Introduce components for Ant -d

import { Modal, Form, Input } from 'antd';
Copy the code

Write to the content page

    <>
      <Table columns={columns} dataSource={props.login.listData} rowKey="id" />
      <UseModal showModal={showModal} visible={visible} />
    </>
Copy the code

The content of the UseModal file is available on demand from Ant -D, so I won’t show you the code here. If you want to know more, you can get it directly from github library.

What I mainly show here is the process of using UMI for warehouse and page interaction. Knowing this process, the code is normal business logic.

It’s best to look at the code I’ve written together with the presentation of the page, otherwise it might get confusing. I have written comments in the source code, very detailed.

My current page looks like this

So what I want to do is change the name of the page by clicking OK, and the interface in the background looks like this

http://public-api-v1.aspirantzhang.com/users/? id=xx data:{name:' xxx'} method:PUTCopy the code

So I need to trigger a commit when I click OK, and it is recommended to commit using umiJS, which uses Dispatch to make changes to the data in the common repository.

So the flow of data here is

Page Dispath ==> Service ==> Effects ==> Reducers ==> pageCopy the code

The first step is to write the dispatch function on the page

The dispatch function can be obtained from props

Const onSubmit = (values) => {const {id} = record; SetConfirmLoading (true); SetTimeout (() => {// this is the logic of the page dispatch. // This is the logic of the page dispatch. 'login/asyncEditName', payload: { id: id, ... values }, }); // Return to false setConfirmLoading(false); // Pop-ups disappear showModal(); }, 1000); };Copy the code

[namespace]/functionName [namespace]/functionName]

The second step is to write axios code in the Service

export async function EditName(id, data) {
  await axios({
    url: `http://public-api-v1.aspirantzhang.com/users/${id}`.method: 'PUT'.data: data,
  }).then(null.(error) = > {
    throw new Error(error);
  });
}
Copy the code

For the ID and data parameters, we can pass the second and third parameters of effect.call.

The third step is to write the logic in Effects

The second argument to call is passed to the function inside the service that takes the first argument

    *asyncEditName({ payload }, effects) {
      {id: XXX,payload:{name: XXX}}
      yield effects.call(EditName, payload.id, payload);
      // Put a new request to render the page, and let the page send another request to fetch data
      yield effects.put({ type: 'asyncGetData' });
    },
Copy the code

The result is something like this

Page Loading state

In a page, we often encounter an interaction that needs to be refreshed after loading, where the state can be obtained through the state passed to the page, which can be obtained in mapStateToProps

const mapStateToProps = (state) = > {
  const { login, loading } = state;
  // Loading Can obtain the loading state of asynchronism
  return { login, userLoading: loading.models.login };
};
export default connect(mapStateToProps)(Login);
Copy the code

Once you get this number, you can use it on the page.

If you don’t want to do this, use the hooks of useSelector to get the values in state.

Annoying the connect

I got bored with Connect after writing a few components, whereas I could have used hooks when using React-Redux alone.

I tried two hooks and found that UMI had already wrapped them so that I could call the hooks directly from the function component without writing connect.

The provider and createStore are no longer needed, as they were done in model time.

Usage:

import { useDispatch, useSelector, Dispatch } from 'dva';

const Login = () = > {
  // Don't use connect
  const dispatch = useDispatch<Dispatch>();
  const { login, loading } = useSelector((state: any) = > {
    returnstate; }); ...Copy the code

The latter

Now the basic flow has gone, it is suggested to follow the flow to knock a code to deepen the impression, also can understand umi’s concept of data flow.

As for the business framework, I think UMI has encapsulated a set of best practices very well. The company is currently using this set of development back-end management systems, and the experience is good, and UMI itself is a bit of template writing. If your team thinks the React stack is too flexible, Projects are difficult to maintain and manage, so try this integration solution. I believe it will bring you a different surprise.

I’ll see you next time.