Handwritten redux connect method, using subscribe to get the latest data

1. Public method files

1. connect file

import React, { useState } from "react";
import MyContext from "./MyContext";
import _ from "lodash";

// Simulate the connect high-order function of react-redux
const connect = (mapStateToProps, mapDispatchToProps) => {
  return (Component) => props => wrapper(Component, {mapStateToProps, mapDispatchToProps, ...props});
};

const wrapper = (Comp, props) => {
  const {mapStateToProps, mapDispatchToProps, ...rest} = props
  
  return <MyContext.Consumer>
    {store => {
      const dispatch = _.get(store, 'dispatch');
      const dispatches = mapDispatchToProps(dispatch);
      // store.subscribe listens to store.getState() to get the latest value
      const [states, setStates] = useState({})
      store.subscribe(() => {
        const state1 = store.getState();
        setStates(mapStateToProps(state1))
      });
      
      return <Comp {...{...states, ...dispatchs, ...rest}}/>;
    }}
  </MyContext.Consumer>
  
}

export default connect;

2. MyContext file:

import React from "react";
import createContext from './createContext'

const MyContext = createContext({});

export default MyContext

3. createContext file:

import React from "react";

const createContext = ({}) => {
  let value = {};

  const Provider = (props) => {
    value = props.value;

    return <>{props.children}</>;
  };

  const Consumer = ({ children }: { children: any }) => {
    return <>{typeof children === "function" ? children(value) : children}</>;
  };

  return { Provider, Consumer };
};

export default createContext;

4. reducer file

//Add list data and change array data
// Split the business logic into a separate file to facilitate status management
import _ from 'lodash';

export interface StateProps {
    id: number;
    text: string;
    isFinished: boolean;
  }
  export interface ActionProps {
    type: string;
    [key: string]: any;
  }
  
  interface IStateObjectProps {
    pickerArr: StateProps[];
    filterTag: 'SHOW_ALL'|'SHOW_FINISHED'|'SHOW_NOT_FINISH';
    dispatch: any;
  }
const reducer = (state: IStateObjectProps, action: ActionProps) => {
    console.log(state, action, 'reducer');
    const pickerArr0 = _.get(state, 'pickerArr')||[];
    switch (action.type) {
      case "ADD":
        return {
            ...state,
            pickerArr: [...pickerArr0, _.get(action, 'todo')]
        };
      case "CHANGESTATUS":
        const pickerArr = _.map(pickerArr0, (item) => {
          if (item.id === action.id) {
            return Object.assign({}, item, { isFinished: !_.get(item, 'isFinished') });
          }
          return item;
        })||[];
        return {
            ...state,
            pickerArr,
        }
      case 'SET_VISIBILITY_FILTER':
        const filterTag = action.filterTag;
        return {
            ...state,
            filterTag,
        };
      default:
        return state || {};
    }
  };

  export default reducer

5. mapStateToProps file:

import React from "react";
import _ from "lodash";
import store from "./store";

 // Different types of todo lists
 const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case "SHOW_ALL": // Show all
      return todos;
    case "SHOW_FINISHED":
      return todos.filter((t) => t.isFinished);
    case "SHOW_NOT_FINISH":
      return todos.filter((t) => !t.isFinished);
    default:
      return todos;
  }
};

export const mapStateTotProps = (state) => {
  // console.log(state, 'mapStateTotProps', store)
  return {
    todoList: getVisibleTodos(_.get(state, 'pickerArr')||[], _.get(state, 'filterTag'))|| [],
  }
}

6. mapDispatchToProps file

import React from "react";
import _ from "lodash";
import { StateProps } from "./reducer";

export const mapDispatchToProps = (dispatch) => {
  // console.log(dispatch, 'mapDispatchToProps============')
  // Filter todo list
  const onFilterTodoList = (filterTag) => {
    dispatch({ type: 'SET_VISIBILITY_FILTER', filterTag, });
  };
  const changeTodo = (id: number) => {
    dispatch({ type: "CHANGESTATUS", id: id });
  };
  // add todo
  const addTodo = (todo: StateProps) => {
    dispatch({ type: "ADD", todo });
  };
  const showAll = () => onFilterTodoList("SHOW_ALL");
  const showFinished = () => onFilterTodoList("SHOW_FINISHED");
  const showNotFinish = () => onFilterTodoList("SHOW_NOT_FINISH");
  
  return {
    changeTodo,
    addTodo,
    showAll,
    showFinished,
    showNotFinish,
  };
}

It can be seen from the mapStateToProps file and mapDispatchToProps file that we need to find a way to obtain the latest state and a common dispatch method, which is the default export object in the store file mentioned below:

7. store files:

import React from 'react';
import reducer from './reducer'

function createStore(reducer) {
    let state = null;
    const listeners = [];
    const subscribe = (fn) => listeners.push(fn);
    const getState = () => state;
    const dispatch = (action) => {
        const state1 = reducer(state, action);
        state = state1
        // Because there is a monitoring callback executed after the latest state value is obtained, you can use store.subscribe to monitor the latest state value!!!
        listeners.forEach((fn) => fn());
        return state
    }
    // dispatch({})
    return { getState, dispatch, subscribe, reducer }
}

const store = createStore(reducer)

console.log(store.getState(), 'oldState======')
store.subscribe(() => {
    const newState = store.getState()
    //The data may change, you need to monitor the latest
    console.log(newState, 'newState====');
})


export default store;

8. ContextProvider component:

import React from "react";
import MyContext from "./MyContext";
import store from "./store";
import _ from "lodash";

// parent component
const ContextProvider = ({ children }) => {
  return <MyContext.Provider value={store}>{children}</MyContext.Provider>;
};

export default ContextProvider;

2. Using public files

1. TodoInput component

import React, { useState } from "react";
import "./TodoInput.scss";
import connect from './connect';
import { mapStateTotProps } from "./mapStateToProps";
import { mapDispatchToProps } from "./mapDispatchToProps";

// Subassembly
const TodoInput = (props) => {
  const [text, setText] = useState("");
  const {
    addTodo,
    showAll,
    showFinished,
    showNotFinish,
  } = props;

  const handleChangeText = (e: React.ChangeEvent) => {
    setText((e.target as HTMLInputElement).value);
  };

  const handleAddTodo = () => {
    if (!text) return;
    addTodo({
      id: new Date().getTime(),
      text: text,
      isFinished: false,
    });
    setText("");
  };

  return (
    <div className="todo-input">
      <input
        type="text"
        placeholder="Please enter to-do items"
        onChange={handleChangeText}
        value={text}
      />
      <button onClick={handleAddTodo}> + Add</button>
      <button onClick={showAll}>show all</button>
      <button onClick={showFinished}>show finished</button>
      <button onClick={showNotFinish}>show not finish</button>
    </div>
  );
};

export default connect(mapStateTotProps, mapDispatchToProps)(TodoInput);

2. TodoList component

import React from "react";
import TodoItem from "./TodoItem";
import _ from "lodash";
import connect from "./connect";
import { mapStateTotProps } from "./mapStateToProps";
import { mapDispatchToProps } from "./mapDispatchToProps";

const TodoList = (props) => {
  const { todoList } = props;

  return (
    <>
      <p>checkbox-list:</p>
      <div className="todo-list">
        {_.map(todoList, (item) => (
          <TodoItem key={_.get(item, "id")} todo={item || {}} />
        ))}
      </div>
      <hr />
    </>
  );
};

export default connect(mapStateTotProps, mapDispatchToProps)(TodoList);

3. TodoItem component

import _ from 'lodash';
import React from "react";
import connect from './connect';
import { mapStateTotProps } from "./mapStateToProps";
import { mapDispatchToProps } from "./mapDispatchToProps";

// grandson component
const TodoItem = (props: any) => {
    const { todo, changeTodo } = props;
    //Change event status
    const handleChange = () => {
        changeTodo(_.get(todo, 'id'));
    }
    
    return (
        <div className="todo-item">
            <input type="checkbox" checked={todo.isFinished} onChange={handleChange} />
            <span style={<!-- -->{ textDecoration: _.get(todo, 'isFinished') ? 'line-through' : 'none' }}>{todo.text}</span>
        </div>
    )
}

export default connect(mapStateTotProps, mapDispatchToProps)(TodoItem);

4. Todo component:

import React from "react";
import TodoInput from "./TodoInput";
import TodoList from "./TodoList";

// parent component
const Todo = () => {
  return (
    <>
      <TodoInput />
      <TodoList />
    </>
  );
};
export default Todo;

5. The App component uses ContextProvider to wrap the Todo component

import React from "react";
import Todo from './mockConnectProvider/Todo'
import ContextProvider from './mockConnectProvider/ContextProvider'

const App: React.FC = () => {
  return (
    <ContextProvider>
      <Todo />
    </ContextProvider>
  );
};

export default App;

The rendering is as follows:

3. Use recoil to simulate connect

1. App.tsx file

import React from "react";
import Todo from "./mockConnectProvider/Todo";
import { RecoilRoot } from "recoil";

const App: React.FC = () => {
  return (
    <RecoilRoot>
      <Todo />
    </RecoilRoot>
  );
};

export default App;

2. Todo.tsx

import React from "react";
import TodoInput from "./TodoInput";
import TodoList from "./TodoList";
import ContextProvider from "./ContextProvider";

// parent component
const Todo = () => {
  return (
    <ContextProvider>
      <TodoInput />
      <TodoList />
    </ContextProvider>
  );
};
export default Todo;

3. connect.tsx

import React from "react";
import MyContext from "./MyContext";
import _ from "lodash";
import { atom, useRecoilState } from "recoil";

const connectState1 = atom({
  key: "connect-state1",
  default: {},
});
// Simulate the connect high-order function of react-redux
const connect = (mapStateToProps, mapDispatchToProps) => {
  return (Component) => (props) =>
    wrapper(Component, { mapStateToProps, mapDispatchToProps, ...props });
};

const useReducer = (store) => {
  const [state, setState] = useRecoilState(connectState1);

  const dispatch = (action) => {
    const state1 = store.dispatch(action);
    setState((state0: any) => ({
      ...state0,
      ...state1,
    }));
  };
  return [state, dispatch];
};

const wrapper = (Comp, props) => {
  const { mapStateToProps, mapDispatchToProps, ...rest } = props;

  return (
    <MyContext.Consumer>
      {(store) => {
        const [state, dispatch] = useReducer(store);
        const dispatches = mapDispatchToProps(dispatch);
        let states1 = mapStateToProps(state);

        return <Comp {...{ ...states1, ...dispatchs, ...rest }} />;
      }}
    </MyContext.Consumer>
  );
};

export default connect;

For other files, just take the first and second files

Principle:

1) The RecoilRoot tag is similar to the Provider tag of redux. With it, it facilitates global component communication.

2) The useRecoilState hook can set global variables under the same source. Naturally, if one modification is made, all components can trigger updates.

4. Context simulates connect

1. App.tsx

import React from "react";
import Todo from "./mockConnectProvider/Todo";

const App: React.FC = () => {
  return (
    <>
      <Todo />
    </>
  );
};

export default App;

2. Todo.tsx

import React from "react";
import TodoInput from "./TodoInput";
import TodoList from "./TodoList";
import ContextProvider from "./ContextProvider";

// parent component
const Todo = () => {
  return (
    <ContextProvider>
      <TodoInput />
      <TodoList />
    </ContextProvider>
  );
};
export default Todo;

3. MyContext file

import React from "react";

const MyContext = React.createContext({});

export default MyContext

4. ContextProvider.tsx

import React, { useContext } from "react";
import MyContext from "./MyContext";
import _ from "lodash";
import reducer from "./reducer";

// parent component
const ContextProvider = ({ children }) => {
  const context:any = useContext(MyContext);
  const [state, dispatch] = React.useReducer(reducer, context);
  
  return <MyContext.Provider value={<!-- -->{getState:() => state, dispatch}}>{children}</MyContext.Provider>;
};

export default ContextProvider;

5. connect.tsx

import React from "react";
import MyContext from "./MyContext";
import _ from "lodash";

// Simulate the connect high-order function of react-redux
const connect = (mapStateToProps, mapDispatchToProps) => {
  return (Component) => (props) =>
    wrapper(Component, { mapStateToProps, mapDispatchToProps, ...props });
};

const wrapper = (Comp, props) => {
  const { mapStateToProps, mapDispatchToProps, ...rest } = props;

  return (
    <MyContext.Consumer>
      {(store) => {
        const dispatches = mapDispatchToProps(_.get(store, 'dispatch'));
        let states1 = mapStateToProps(_.get(store, 'getState') ? _.get(store, 'getState')(): {});

        return <Comp {...{ ...states1, ...dispatchs, ...rest }} />;
      }}
    </MyContext.Consumer>
  );
};

export default connect;

6. Find other required component files from 1 and 2,

Note: This method does not require a store file.