ctx + hooks 实现一个最简版的redux

177 阅读1分钟

创建上下文

import React, { 
    createContext, useContext, useRef, 
    useMemo, forwardRef, useReducer 
} from 'react';
import { shallowEqual } from 'react-redux';

const Ctx = createContext<any>({});

export const Provider = (props: any) => {
    const ref = useRef<any>();
    ref.current = props.reducers;
    const [state, dispatch] = useReducer((state: any, action: any) => {
        const { type, ...actionData } = action;
        const reducer = ref.current[type];
        const result = reducer(state, actionData);

        if (!shallowEqual(result, state)) {
            return result;
        }

        return state;
    }, props.initialState);

    const store = useMemo(() => {
        return { state, dispatch };
    }, [state]);

    return <Ctx.Provider value={store}>{props.children}</Ctx.Provider>;
};

export const useCtx = () => {
    return useContext(Ctx);
};

selector +connect

export const useSelector = (selector: any, props?: any) => {
    const ref = useRef<any>();
    ref.current = selector;
    const { state } = useCtx();

    return useMemo<any>(() => {
        return ref.current(state, props);
    }, [state, props]);
};

export const connect = (mapStateToProps?: any, mapDispatchToProps: any) => {
    return (Comp: React.ComponentType) => {
        return forwardRef((props, ref) => {
            const dispatchRef = useRef<any>();
            const ctx = useCtx();
            dispatchRef.current = mapDispatchToProps(ctx.dispatch, props);
            const state = useSelector(mapStateToProps, props);
            return useMemo(() => {
                const fullProps = { 
                    ...props, 
                    ...(state || {}), 
                    ...(dispatchRef.current || {}) 
                };
                return <Comp {...fullProps} ref={ref} />;
            }, [props, ref, state]);
        });
    };
};

使用

components.jsx

import { useCtx, useSelector, connect } from './ctx';

export const AddBtn = () => {
    const ctx = useCtx();

    return (
        <section>
            <button
                onClick={() => {
                    ctx.dispatch({ type: 'add', value: Math.random() });
                }}
            >
                click
            </button>
        </section>
    );
};

export const ShowValue = () => {
    const ctx = useCtx();
    return (
        <section>
            <span>{JSON.stringify(ctx.state)}</span>
        </section>
    );
};

let renderConter = 0;
export const EmptyListener = () => {
    const ctx = useSelector();
    console.log('log---------empty', ctx);
    renderConter++;
    return (
        <section style={{ marginTop: 10 }}>
            <ul>
                <li>使用useSelector一定会触发渲染,因为内部对引用了ctx</li>
            </ul>
            <span>empty listener:{renderConter}</span>
        </section>
    );
};

export const SelectorComp = connect((state) => {
    return state.value
})((props: any) => {
    console.log('log---------SelectorComp', props);
    return (
        <section>
            <span>SelectorComp:{renderConter}</span>
        </section>
    );
});

index

import { Provider } from './ctx';
import { 
    AddBtn, ShowValue, EmptyListener, SelectorComp 
} from './components';

export default function CtxWithHooks() {
    return (
        <Provider
            reducers={{
                add: (state: any, data: any) => ({ ...state, value: data.value }),
            }}
            initialState={{ value: 1 }}
        >
            <AddBtn />
            <ShowValue />
            <EmptyListener />
            <SelectorComp />
        </Provider>
    );
}