Skip to content

React personal implementation version with unit test.

License

Notifications You must be signed in to change notification settings

caiyongmin/tiny-react

Repository files navigation

creact

npm version codecov PRs Welcome

React personal implementation version.

Run

yarn

# need install parcel:
# - yarn global add parcel-bundler
# - npm install -g parcel-bundler
npm start

Features

  • Familiar React render component with virtual dom.
  • Support useState hook.

Todos

  • Support render Class Component.
  • Support Class Component setState and lifecycle api.
  • Support other hooks api.
    • support useEffect.
    • support useReducer.
    • support useCallback / useMemo / useRef.
    • support useContext.
  • Publish package.
  • Add unit test.
  • Clarify the code design and add necessary comments.

Examples

render Function Component and Class Component

↥ back to examples

Edit creact-simple-demo

Function Component

import { React, useState } from "@caiym/react";

function Event() {
  return <span>Event</span>;
}

function Log() {
  return <span>Log</span>;
}

export default function HooksFunction() {
  const [toggle, setToggle] = useState(false);

  return (
    <div>
      <button
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        useState toggle
      </button>
      &nbsp;&nbsp;
      {toggle ? <Event /> : <Log />}
    </div>
  );
}

Class Component

import { React } from "@caiym/react";

type ItemType = {
  id: number;
  text: string;
};

interface TodoAppProps {
  title: string;
}

interface TodoAppState {
  text: string;
  items: ItemType[];
}

class TodoApp extends React.Component<TodoAppProps, TodoAppState> {
  constructor(props: TodoAppProps) {
    super(props);

    this.state = {
      items: [],
      text: "",
    };
  }

  handleChange = (e: MouseEvent) => {
    const target = e.target as HTMLInputElement;
    this.setState({ text: target.value });
  }

  handleSubmit = (e: MouseEvent) => {
    const { text } = this.state;

    e.preventDefault();
    if (!text.length) {
      return;
    }
    const newItem = {
      text,
      id: Date.now(),
    };
    this.setState((state: TodoAppState) => ({
      items: state.items.concat(newItem),
      text: '',
    }));
  }

  render() {
    const { title } = this.props;
    const { items, text } = this.state;

    return (
      <div>
        <h3>{title}</h3>
        <TodoList items={items} />
        <form>
          <label htmlFor="new-todo">What needs to be done?</label>
          <br />
          <input
            autocomplete="off"
            id="new-todo"
            onChange={this.handleChange}
            value={text}
          />
          &nbsp;&nbsp;
          <button onClick={this.handleSubmit}>Add #{items.length + 1}</button>
        </form>
      </div>
    );
  }
}

interface TodoListProps {
  items: ItemType[];
}

class TodoList extends React.Component<TodoListProps> {
  render() {
    const { items } = this.props;

    return (
      <ol>
        {items.map((item: ItemType) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ol>
    );
  }
}

export default TodoApp;

Render

import { React, ReactDOM } from "@caiym/react";
import FunctionComponent from "./FunctionComponent";
import ClassComponent from "./ClassComponent";

class App extends React.Component {
  render() {
    return (
      <div>
        <FunctionComponent />
        <ClassComponent title="React Todo" />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

render useState Component

↥ back to examples

Edit creact useState demo

import { useState } from "@caiym/react";

function UseStateComponent() {
  const initialValue = 0;
  const [ count, setCount ] = useState(initialValue);

  return (
    <div>
      <h3>useState</h3>
      <div>
        <button onClick={() => setCount(count + 1)}>setCount</button>
        <button onClick={() => setCount(initialValue)}>reset</button>
        &nbsp;&nbsp;
        {count}
      </div>
    </div>
  );
}

render useEffect Component

↥ back to examples

Edit creact useEffect demo

import { useState, useEffect } from "@caiym/react";

function UseEffectComponent() {
  const [ toggle, setToggle ] = useState(false);
  const [ count, setCount ] = useState(0);

  useEffect(() => {
    console.info('===run useEffect function===');
    return () => {
      console.info('===cleanup before re-run useEffect function===');
    };
  }, [toggle]);

  return (
    <div>
      <h3>useEffect</h3>
      <div style={{ fontSize: '12px', color: 'gray' }}>需要打开控制台查看运行结果</div>
      <button onClick={() => setToggle(!toggle)}>
        setToggle trigger run useEffect function, toggle: {String(toggle)}
      </button>
      <div>
        <button onClick={() => setCount(count + 1)}>
          setCount don't trigger run useEffect function, count: {count}
        </button>
      </div>
    </div>
  );
}

render useReducer Component

↥ back to examples

Edit creact useReducer demo

import { useReducer } from "@caiym/react";

const sleep = (ms: number = 500) => new Promise((resolve) => setTimeout(resolve, ms));

type ReducerState = {
  count: number;
  loading: boolean;
};
const ACTIONS = {
  INCREASE: 'increase',
  DECREASE: 'decrease',
  LOADING: 'loading',
  RESET: 'reset',
};
const initialState = {
  count: 0,
  loading: false,
};
const countReducer = (state: ReducerState, action: { type: string; [key: string]: string }) => {
  switch (action.type) {
    case ACTIONS.INCREASE:
      return { ...state, loading: false, count: state.count + 1 };
    case ACTIONS.DECREASE:
      return { ...state, loading: false, count: state.count - 1 };
    case ACTIONS.LOADING:
      return { ...state, loading: true };
    case ACTIONS.RESET:
      return { ...state, loading: false, count: initialState.count };
    default:
      return state;
  }
}
function UseReducerComponent() {
  const [ state, dispatch ] = useReducer(countReducer, initialState);
  const { count, loading } = state;
  const onIncreaseHandler = async () => {
    dispatch({ type: ACTIONS.LOADING });
    await sleep();
    dispatch({ type: ACTIONS.INCREASE });
  };
  const onDecreaseHandler = async () => {
    dispatch({ type: ACTIONS.LOADING });
    await sleep();
    dispatch({ type: ACTIONS.DECREASE });
  };
  const onResetHandler = async () => {
    dispatch({ type: ACTIONS.LOADING });
    await sleep();
    dispatch({ type: ACTIONS.RESET });
  };

  return (
    <div>
      <h3>useReducer</h3>
      <div className="result">Count: {loading ? 'loading...' : count}</div>
      <div className="operators">
        <button onClick={onIncreaseHandler}>+</button>
        <button onClick={onDecreaseHandler}>-</button>
        <button onClick={onResetHandler}>reset</button>
      </div>
    </div>
  );
}

render useCallback Component

↥ back to examples

Edit creact useCallback demo

import { useCallback, useState } from "@caiym/react";

function useInputValue(initialValue: string) {
  const [ value, setValue ] = useState(initialValue);
  // stable onChange prop, avoid unnecessary render
  const onChange = useCallback((event: MouseEvent) => {
    const target = event.target as HTMLInputElement;
    setValue(target.value);
  }, []);

  return {
    value,
    onChange,
  };
}
function UseCallbackComponent() {
  const name = useInputValue('Jack');

  return (
    <div>
      <h3>useCallback</h3>
      <input type="text" {...name} />
      <div>Value: {name.value}</div>
    </div>
  );
}

render useMemo Component

↥ back to examples

In fact, the example is't very suitable, because re-render of MemoChild component has been avoided by props comparison. For a better example, please look at the example of render useContext Component.

Edit creact useMemo demo

import { useMemo, useState } from "@caiym/react";

function MemoChild(props: {
  count: number;
}) {
  return useMemo(() => (
    <div>MemoChild: {+new Date()}</div>
  ), [props.count]);
}
function UseMemoComponent() {
  let [ count, setCount ] = useState(0);
  let [ num, setNum ] = useState(0);

  return (
    <div>
      <h3>useMemo</h3>
      <div>Count: {count}</div>
      <div>Number: {num}</div>
      <MemoChild count={count} />
      <button onClick={() => setCount(count + 1)}>
        setCount to trigger MemoChild re-render
      </button>
      <br/>
      <button onClick={() => setNum(num + 1)}>
        setNum don't trigger MemoChild re-render
      </button>
    </div>
  );
}

render useRef Component

↥ back to examples

Edit creact useRef demo

import { useRef } from "@caiym/react";

function UseRefComponent() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };

  return (
    <div>
      <h3>useRef</h3>
      <input ref={inputEl} type="text"/>
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}

render useContext Component

↥ back to examples

Edit creact useContext demo

import { React, useState, useContext } from "@caiym/react";

const CounterContext = React.createContext(0);
function ContextChild() {
  const count = useContext(CounterContext);
  return (
    <div>count: {count}</div>
  );
}
function ContextMemoChild(props: {
  count: number;
}) {
  return useMemo(() => (
    <div>MemoChild: {+new Date()}</div>
  ), [props.count]);
}
function CommonChild(props: {
  count: number;
}) {
  return <div>CommonChild: {+new Date()}</div>;
}

function UseContextComponent() {
  const [ count, setCount ] = useState(0);
  return (
    <div>
      <h3>useContext</h3>
      <CounterContext.Provider value={count}>
        <ContextChild />
        <div>
          <ContextChild />
          <ContextMemoChild count={0} />
          <CommonChild count={0} />
        </div>
      </CounterContext.Provider>
      <button onClick={() => setCount(count + 1)}>setCount</button>
    </div>
  );
}

Refs

Thank you here!

Welcome to commit issue & pull request !

About

React personal implementation version with unit test.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages