20200720のReactに関する記事は7件です。

[React] useStateのセッターで配列を更新しても再レンダリングされない時

前置き

Reactのstate hookで配列を定義していて、更新したいと思いセッターに値を渡すと中身は更新されているのにコンポーネントが再レンダリングされない事案にぶち当たりました。

const [hoge,setHoge] = React.useState<SampleType>(InitialArray)

これに悩まされて数時間無駄にしたので誰かのお役に立てば...

問題のコード

問題となるコードのサンプルを作ってみました

/*--------------------------------略*/
const LunchList: React.FC = () => {
  const [lunchlist,setList] = React.useState<Lunch[]>(InitialArray)

  React.useEffect(()=>{
    const setvalue = lunchlist
    setvalue.push('パスタ') //重要
    setList(setvalue)
  })
/*--------------------------------略*/
}

パット見動きそうなのですがこれだと前置きで話した通り再レンダリングされません。

なぜ?

調べてみたところReactのstate hookは

object.is()

を使って変更があったかどうかを判別しているので、
今回の例のようにlunchlistをコピーしたsetvalue
push() などで直接操作してセッターに渡しても再レンダリングされないようです。

公式の記事
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

解決

オリジナルでもコピーでもダメなら新しい配列をつくってしまう

const setvalue = [...lunchlist, 'パスタ'] //解決
setList(setvalue)

こうしてしまえばobject.is()で変更があったか判別できるので再レンダリングしてもらえます

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでhello worldするまでを追ってみた

Reactでhello worldされるまでのコードを追ってみました。
React.createElementとReactDOM.renderの流れを追いたいと思います。
間違っている箇所などコメントいただけるととても喜びます。

前提

  • 以下のような箇所は条件分岐されているような箇所は飛ばします。
    • __DEV__enableSchedulerTracing変数で条件分岐されている
  • コメントアウトは削除します。
  • 読みやすさのためにコードを省力している部分は、以下のようなコメントアウトをします。
// --- 省略した処理の説明 --- 

使用したもの

  • chromeのdevtoolsで確認しやすいようにsetTimeoutが挟まれてます。
index.dev.jsx
import React from 'react';
import ReactDOM from 'react-dom';

function App (props) {
  return (
    <div>
      Hello world
    </div>
  );
}

window.addEventListener('load', function () {
  setTimeout(function () {
    ReactDOM.render(
      <App/>,
      document.querySelector('#app-root')
    );
  }, 2000);
});
package.json
{
  "name": "experiment",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --progress --color --config ./webpack.config.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.5",
    "react": "^16.8.1",
    "react-dom": "^16.8.1",
    "webpack": "^4.29.3",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.1.14"
  }
}

評価されるコード

  • babelを通したコードです。
  • 以下の順番で実行をみていきます。
    • 1. React.createElement(App, null)
    • 2. ReactDOM.render(<App/>, document.querySelector('#app-root')
"use strict";

var _react = _interopRequireDefault(require("react"));

var _reactDom = _interopRequireDefault(require("react-dom"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function App(props) {
  return _react.default.createElement("div", null, "Hello world");
}

window.addEventListener('load', function () {
  setTimeout(function () {
    _reactDom.default.render( // <= 2
      _react.default.createElement(App, null), // <= 1
      document.querySelector('#app-root')
    );
  }, 2000);
});

1. React.createElement(App, null)

process

- createElement
    - return ReactElement

createElement

  • まずはReact.createElement実行していきます。
  • createElementは引数を元にごにょごにょしてパラメータを整えて、Elementの生成処理をReactElement関数に任せ、その返り値をそのまま返します。
  • 今回渡す引数にはpropsもchildrenもないので、ReactElementに渡す引数は非常に寂しいものになりました。
  • ReactCurrentOwner.currentという奴が出てきましたが、こちらはFiberのあたりを読んでみないとよくわからなそうです。

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react/src/ReactElement.js#L171

// type = App
// config = null
// children = undefined
function createElement (type, config, children) {

  // --- arrange parameter for ReactElement ---

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );

}

ReactElement

  • 続いてReactElementの呼び出しです。こちらは基本的に受け取ったデータをオブジェクトに詰めることと、$$typeofというスペシャルなプロパティを定義するだけです。

https://github.com/facebook/react/blob/3ae94e1885b673543a30a05906c4f9a0e4b682cb/packages/react/src/ReactElement.js#L111

// type,                     = App
// key,                      = null
// ref,                      = null
// self,                     = null
// source,                   = null
// ReactCurrentOwner.current = null
// props,                    = {}
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  // --- dev: awesome codes ---

  return element;
};
  • そして以下がcreateElementの今回の返り値になります。以降root-ReactElementと呼びます。
  • こちらは次のReactDOM.renderの第一引数にそのまま渡されます。
// root-ReactElement
{
  $$typeof: Symbol(react.element),
  type: App,
  key: null,
  ref: null,
  props: {},
  _owner: null,
}

2. ReactDOM.render(<App/>, document.querySelector('#app-root'))

  • 次にmountをするためにReactDOM.render関数を実行していきます。
  • こちらは結構処理が多いので、小分けにしていきたいと思います。

process

- ReactDOM.render
    - legacyRenderSubtreeIntoContainer
        - legacyCreateRootFromDOMContainer     <= 2-1
          - ReactRoot
        - ReactRoot.prototype.render           <= 2-2
            - updateContainer
              - scheduleRootUpdate
                - enqueueUpdate                <= 2-2-1
                - scheduleWork                 <= 2-2-2
                  ...
                  - performWork                <= 2-2-2-1
                      ...
                      - renderRoot             <= 2-2-2-1-1
                        ...
                        - performUnitOfWork    <= 2-2-2-1-1-1
                          ...
                          - beginWork          <= 2-2-2-1-1-2
                          - completeUnitOfWork <= 2-2-2-1-1-3
                      - completeRoot           <= 2-2-2-1-2

ReactDOM.render

  • まずはrender関数の実行です。第一引数には前節で生成されたReactElementのroot-ReactElementが渡されています。
  • 第3引数のcallbackは渡していないのでundefinedです。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-dom/src/client/ReactDOM.js#L673

  render(
    element: React$Element<any>, // root-ReactElement
    container: DOMContainer,     // div#app-root
    callback: ?Function,         // undefined
  ) {

    // --- dev: warning ---

    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },

legacyRenderSubtreeIntoContainer

  • 次にlegacyRenderSubtreeIntoContainerです。
  • 初回のrenderのため、legacyCreateRootFromDOMContainerという関数を呼び出し、ReactRootインスタンスを生成し、インスタンスのlegacy_renderSubtreeIntoContainerメソッドを呼び出し、mountの処理をさらに進めていきます。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-dom/src/client/ReactDOM.js#L540

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>, // null
  children: ReactNodeList,                     // root-ReactElement
  container: DOMContainer,                     // div#app-root
  forceHydrate: boolean,                       // false
  callback: ?Function,                         // undefined
) {
  // --- dev: warning ---

  let root: Root = (container._reactRootContainer: any);
  if (!root) {
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer( // <= 2-1
      container,
      forceHydrate,
    );

    // --- arrange callback ---

    unbatchedUpdates(() => {
      if (parentComponent != null) {
        // --- process when parentComponent is not null ---
      } else {
        root.render(children, callback);                                     // <= 2-2
      }
    });
  } else {

    // --- process when render is not initial ---

  }
  return getPublicRootInstance(root._internalRoot);
}

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-dom/src/client/ReactDOM.js#L495

2-1 legacyCreateRootFromDOMContainer

  • 渡されたDOMのchildrenの掃除とReactRootの生成を担います。
  • 生成物をそのまま返します。
function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): Root {
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {

      // --- dev: warn on SSR related ---

      container.removeChild(rootSibling);
    }
  }

  // --- dev: warn on hydrate ---

  const isConcurrent = false;
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

2-1-1 ReactRoot

  • ReactRootというものを生成するために、react-reconcilerの方へ飛んだりします。
  • _internalRootプロパティにはFiberRootなる全てのFiberの親っぽいものが突っ込まれます。
  • こちらはcurrentのプロパティにHostRootのtagをもつFiberを持ちます。
    • こちらのFiberは以降root-HostRootFiberと呼びます。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-dom/src/client/ReactDOM.js#L365

function ReactRoot(
  container: DOMContainer, // 
  isConcurrent: boolean,
  hydrate: boolean,
) {
  const root = createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}


// react-reconciler from here

export function createContainer(
  containerInfo: Container,
  isConcurrent: boolean,
  hydrate: boolean,
): OpaqueRoot {
  return createFiberRoot(containerInfo, isConcurrent, hydrate);
}

export function createFiberRoot(
  containerInfo: any,
  isConcurrent: boolean,
  hydrate: boolean,
): FiberRoot {
  const uninitializedFiber = createHostRootFiber(isConcurrent);

  let root;
  if (enableSchedulerTracing) {
    // --- root with scheduler tracing ---
  } else {
    root = ({
      current: uninitializedFiber,
      containerInfo: containerInfo,
      pendingChildren: null,

      pingCache: null,

      earliestPendingTime: NoWork,
      latestPendingTime: NoWork,
      earliestSuspendedTime: NoWork,
      latestSuspendedTime: NoWork,
      latestPingedTime: NoWork,

      didError: false,

      pendingCommitExpirationTime: NoWork,
      finishedWork: null,
      timeoutHandle: noTimeout,
      context: null,
      pendingContext: null,
      hydrate,
      nextExpirationTimeToWorkOn: NoWork,
      expirationTime: NoWork,
      firstBatch: null,
      nextScheduledRoot: null,
    }: BaseFiberRootProperties);
  }

  uninitializedFiber.stateNode = root;

  return ((root: any): FiberRoot);
}
// root-HostRootFiber
FiberNode {
  actualDuration: 0
  actualStartTime: -1
  alternate: null
  child: null
  childExpirationTime: 0
  contextDependencies: null
  effectTag: 0
  elementType: null
  expirationTime: 0
  firstEffect: null
  index: 0
  key: null
  lastEffect: null
  memoizedProps: null
  memoizedState: null
  mode: 4
  nextEffect: null
  pendingProps: null
  ref: null
  return: null
  selfBaseDuration: 0
  sibling: null
  stateNode: root,
  tag: 3
  treeBaseDuration: 0
  type: null
  updateQueue: null
  _debugID: 1
  _debugIsCurrentlyTiming: false
  _debugOwner: null
  _debugSource: null
}
  • ここまできて、前述のlegacyRenderSubtreeIntoContainer内のrootの部分は以下となります。
  • 以降こちらのインスタンスをroot-ReactRoot、中身の_internalRootroot-internalRootと省略します。
// root-ReactRoot
ReactRoot {
  // root-internalRoot
  _internalRoot: {
    containerInfo: div#app-root,
    context: null,
    current: root-HostRootFiber,
    didError: false,
    earliestPendingTime: 0,
    earliestSuspendedTime: 0,
    expirationTime: 0,
    finishedWork: null,
    firstBatch: null,
    hydrate: false,
    interactionThreadID: 1,
    latestPendingTime: 0,
    latestPingedTime: 0,
    latestSuspendedTime: 0,
    memoizedInteractions: Set(0) {},
    nextExpirationTimeToWorkOn: 0,
    nextScheduledRoot: null,
    pendingChildren: null,
    pendingCommitExpirationTime: 0,
    pendingContext: null,
    pendingInteractionMap: Map(0) {},
    pingCache: null,
    timeoutHandle: -1,
  }
}

2-2 ReactRoot.prototype.render

  • 前節までで生成されたroot-ReactRootrenderメソッドを呼び出して、いよいよReactをmountしていきます。
  • unbatchedUpdatesという関数が出てきていますが、、今回のケースでは渡された関数をそのまま実行するだけです。
  • ReactWorkのインスタンスも生成して、その_onCommitメソッドをupdateContainerに渡しています。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-dom/src/client/ReactDOM.js#L373

ReactRoot.prototype.render = function(
  children: ReactNodeList, // root-ReactElement
  callback: ?() => mixed,  // undefined
): Work {
  const root = this._internalRoot;
  const work = new ReactWork();
  callback = callback === undefined ? null : callback;
  // --- dev: warning ---
  // --- set callback ---
  updateContainer(children, root, null, work._onCommit);
  return work;
};
  • updateContainerを実行します
  • computeExpirationForFiber関数を実行して、expirationTimeという処理が終わっているべき時間を求めています
    • こちらは以降root-expirationTimeと呼びます
  • scheduleRootUpdateを実行して更新処理を進めます。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberReconciler.js#L283
https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberReconciler.js#L162

export function updateContainer(
  element: ReactNodeList,                      // root-ReactElement
  container: OpaqueRoot,                       // root-internalRoot
  parentComponent: ?React$Component<any, any>, // null
  callback: ?Function,                         // work._onCommit
): ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

export function updateContainerAtExpirationTime(
  element: ReactNodeList,                      // root-ReactElement
  container: OpaqueRoot,                       // root-internalRoot
  parentComponent: ?React$Component<any, any>, // null
  expirationTime: ExpirationTime,              // root-expirationTime
  callback: ?Function,                         // work._onCommit
) {
  const current = container.current;

  // --- dev: warning ---

  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  return scheduleRootUpdate(current, element, expirationTime, callback);
}
// root-expirationTime
1073741823
  • scheduleRootUpdateを実行します。
  • やっていることは
    • updateを作成。
    • flushPassiveEffectsはなんかpassiveなeffectをflushするんでしょう
    • enqueueUpdatecurrentのFiberにupdateを盛る
    • scheduleWorkFiberupdateを実行する
    • と思われます

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberReconciler.js#L115

function scheduleRootUpdate(
  current: Fiber,                 // root-HostRootFiber
  element: ReactNodeList,         // root-ReactElement
  expirationTime: ExpirationTime, // root-expirationTime
  callback: ?Function,            // work._onCommit
) {
  // --- dev: warning ---

  const update = createUpdate(expirationTime);
  update.payload = {element};

  // --- arrange callback ---

  flushPassiveEffects();
  enqueueUpdate(current, update);        <= 2-2-1
  scheduleWork(current, expirationTime); <= 2-2-2

  return expirationTime;
}
  • 途中で出てくるupdateはこんな感じです。以降root-updateと呼びます。
  • こちらを保持するupdateQueueは連結リストであるので、nextのプロパティがありますね。
// root-update
{
  "expirationTime": root-expirationTime,
  "tag": 0,
  "payload": root-ReactElement,
  "callback": null,
  "next": null,
  "nextEffect": null
}

2-2-1 enqueueUpdate

  • enqueueUpdateを呼び出して、fiberupdateQueueに先ほど生成したupdateappendUpdateToQueueによって盛り盛りします。
  • root-HostRootFiberは生成されたばかりでしたので、updateQueueはここでcreateUpdateQueueによって作られます。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactUpdateQueue.js#L220

// fiber  = root-HostRootFiber
// update = root-update
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  const alternate = fiber.alternate;
  let queue1;
  let queue2;
  if (alternate === null) {
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // --- process when there are two fiber nodes ---
  }
  if (queue2 === null || queue1 === queue2) {
    appendUpdateToQueue(queue1, update);
  } else {
    // --- process when there are two updateQueues ---
  }

  // --- dev: warning ---
}
// root-HostRootFiber.updateQueue
{
  "baseState": null,
  "firstUpdate": root-update,
  "lastUpdate": root-update,
  "firstCapturedUpdate": null,
  "lastCapturedUpdate": null,
  "firstEffect": null,
  "lastEffect": null,
  "firstCapturedEffect": null,
  "lastCapturedEffect": null
}

2-2-2 scheduleWork

  • scheduleRootUpdateの実行に戻りまして、次はscheduleWorkを実行します。ここからが長いです。。。
  • scheduleWorkToRootを実行して、rootをgetします。
  • 見つかったrootとそのrootがもつexpirationTimerequestWorkに渡します。
  • if文で出てくるnextRootはこのファイルが持つグローバル変数です。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L1849

// fiber          = root-HostRootFiber
// expirationTime = root-expirationTime
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {
    // --- dev: warning ---
    return;
  }

  // --- process when interruption occuered

  markPendingPriorityLevel(root, expirationTime);
  if (
    !isWorking ||
    isCommitting ||
    nextRoot !== root
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }
  // --- dev: warning ---
}
  • scheduleWorkToRootの実行です
  • 引数のexpirationTimeの方がroot-HostRootFiberのもつexpirationTimeよりも大きいので、この値がセットされます。
  • root-HostRootFiberreturnプロパティがnullかつtagHostRootの値を持っていますので、このfiberのもつstateNode(root-internalRoot)をFiberRootということにしてこの関数の実行を終了します。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L1744

// fiber          = root-HostRootFiber
// expirationTime = root-expirationTime
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
  recordScheduleUpdate();

  // --- dev: warning ---

  // Update the source fiber's expiration time
  if (fiber.expirationTime < expirationTime) {
    fiber.expirationTime = expirationTime;
  }
  // --- process when there is alternate ---
  let node = fiber.return;
  let root = null;
  if (node === null && fiber.tag === HostRoot) {
    root = fiber.stateNode;
  } else {
    // --- process when its not root ---
  }

  // --- scheduler tracing related ---
  return root;
}
  • requestWorkの実行です
  • Syncという変数には31ビット符号あり整数の最大値が入っています。
  • root-expirationTimeにも同様の数値が入っています。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L2084

// isBatchingUpdates = false
// Sync              = 1073741823

// root           = root-ReactRoot
// expirationTime = root-expirationTime
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    return;
  }

  if (isBatchingUpdates) {
    // --- process when batchingUpdates ---
    return;
  }

  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}
  • addRootToScheduleの実行です。ファイルのグローバル変数がごにょごにょされます。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L2112

// firstScheduledRoot = null
// lastScheduledRoot  = null

// root           = root-ReactRoot
// expirationTime = root-expirationTime
function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
  if (root.nextScheduledRoot === null) {
    root.expirationTime = expirationTime;
    if (lastScheduledRoot === null) {
      firstScheduledRoot = lastScheduledRoot = root;
      root.nextScheduledRoot = root;
    } else {
      // --- process when lastScheduledRoot has value ---
    }
  } else {
    // --- process when root has nextScheduledRoot property ---
  }
}
2-2-2-1 performWork
  • requestWorkに処理が戻りまして、performSyncWorkの実行です。こちらはperformWorkのためのシンプルなラッパーです
  • performWorkでは引数でデータが渡されなくなっているのですが、それはグローバル変数を頼りにしているからの模様です。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L2240
https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L2244

function performSyncWork() {
  performWork(Sync, false);
}

// minExpirationTime = Sync
// isYieldy          = false
function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {
  findHighestPriorityRoot();

  if (isYieldy) {
    // --- process when isYieldy ---
  } else {
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      minExpirationTime <= nextFlushedExpirationTime
    ) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
      findHighestPriorityRoot();
    }
  }

  if (isYieldy) {
    // --- process when isYieldy ---
  }
  // If there's work left over, schedule a new callback.
  if (nextFlushedExpirationTime !== NoWork) {
    scheduleCallbackWithExpirationTime(
      ((nextFlushedRoot: any): FiberRoot),
      nextFlushedExpirationTime,
    );
  }

  // Clean-up.
  finishRendering();
}
  • まずはfindHighestPriorityRootを実行してnextFlushedRootnextFlushedExpirationTimeのお世話をします
  • 実行後の値は以下です
nextFlushedRoot           = root-ReactRoot
nextFlushedExpirationTime = root-expirationTime
  • performWorkOnRootの実行です

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L2349

// root           = root-ReactRoot
// expirationTime = root-expirationTime
// isYieldy       = false
function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isYieldy: boolean,
) {
  // --- warning ---
  isRendering = true;

  if (!isYieldy) {
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // --- process ---
    } else {
      root.finishedWork = null;
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {
        root.timeoutHandle = noTimeout;
        cancelTimeout(timeoutHandle);
      }
      renderRoot(root, isYieldy);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        completeRoot(root, finishedWork, expirationTime);
      }
    }
  } else {
    // --- process when isYieldy ---
  }

  isRendering = false;
}
2-2-2-1-1 renderRoot
  • renderRootの実行です
  • DOMのreconcileや、propertyのセットなどこの関数の呼び出しの範囲内で実行されます。
  • 途中でグローバル変数nextUnitOfWorkを生成しています。
  • ごちゃごちゃしてますが、真ん中あたりのwookLoopがほぼ本体です。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L1223

// nextUnitOfWork = null

// root     = root-ReactRoot
// isYieldy = false
function renderRoot(root: FiberRoot, isYieldy: boolean): void {
  // --- warning ---

  flushPassiveEffects();

  isWorking = true;
  const previousDispatcher = ReactCurrentDispatcher.current;
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const expirationTime = root.nextExpirationTimeToWorkOn;

  if (
    expirationTime !== nextRenderExpirationTime ||
    root !== nextRoot ||
    nextUnitOfWork === null
  ) {
    // Reset the stack and start working from the root.
    resetStack();
    nextRoot = root;
    nextRenderExpirationTime = expirationTime;
    nextUnitOfWork = createWorkInProgress(
      nextRoot.current,
      null,
      nextRenderExpirationTime,
    );
    root.pendingCommitExpirationTime = NoWork;

    if (enableSchedulerTracing) {
      // --- process when schedulerTracing ---
    }
  }

  let prevInteractions: Set<Interaction> = (null: any);
  // --- process when schedulerTracing ---

  let didFatal = false;

  startWorkLoopTimer(nextUnitOfWork);

  do {
    try {
      workLoop(isYieldy);
    } catch (thrownValue) {
      // --- recovery process ---
    }
    break;
  } while (true);

  // --- process when schedulerTracing ---

  isWorking = false;
  ReactCurrentDispatcher.current = previousDispatcher;
  resetContextDependences();
  resetHooks();

  // Yield back to main thread.
  if (didFatal) {
    // --- process when fatal ---
    return;
  }

  if (nextUnitOfWork !== null) {
    const didCompleteRoot = false;
    stopWorkLoopTimer(interruptedBy, didCompleteRoot);
    interruptedBy = null;
    onYield(root);
    return;
  }

  const didCompleteRoot = true;
  stopWorkLoopTimer(interruptedBy, didCompleteRoot);
  const rootWorkInProgress = root.current.alternate;
  // --- warning ---

  nextRoot = null;
  interruptedBy = null;

  if (nextRenderDidError) {
    // --- process when error ---
  }

  if (isYieldy && nextLatestAbsoluteTimeoutMs !== -1) {
    // --- process when isYieldy ---
    return;
  }

  // Ready to commit.
  onComplete(root, rootWorkInProgress, expirationTime);
}
// root-WorkInProgress

FiberNode {
  actualDuration: 0
  actualStartTime: -1
  alternate: root-HostRootFiber
  child: null
  childExpirationTime: 0
  contextDependencies: null
  effectTag: 0
  elementType: null
  expirationTime: 1073741823
  firstEffect: null
  index: 0
  key: null
  lastEffect: null
  memoizedProps: null
  memoizedState: null
  mode: 4
  nextEffect: null
  pendingProps: null
  ref: null
  return: null
  selfBaseDuration: 0
  sibling: null
  stateNode: root,
  tag: 3
  treeBaseDuration: 0
  type: null
  updateQueue: root-HostRootFiber.updateQueueと同じ
}
2-2-2-1-1-1 performUnitOfWork
  • 何はともあれwookLoopを実行します。
  • performUnitOfWorkの返り値がnullになるまで回し続けるだけのお仕事です。
  • beginWorkです!こいつは大事やぞ(多分)
  • workLoopは3週します

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L1209

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberScheduler.js#L1147

function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // --- process when isYieldy ---
  }
}

// workInProgress = root-workInProgress
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  const current = workInProgress.alternate;

  startWorkTimer(workInProgress);
  // --- dev: process ---

  // --- dev: set dev property to fiber ---

  let next;
  if (enableProfilerTimer) {
    // --- process when profilerTimer ---
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
  }

  // --- dev: process ---

  if (next === null) {
    next = completeUnitOfWork(workInProgress);
  }

  ReactCurrentOwner.current = null;

  return next;
}
2-2-2-1-1-2 beginWork
  • beginWorkです。
  • 今回のworkInProgressが持っているタグはHostRootにあたります。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberBeginWork.js#L1893

// current              = root-HostRootFiber
// workInProgress       = root-workInProgress
// renderExpirationTime = root-expirationTime
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  const updateExpirationTime = workInProgress.expirationTime;

  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (oldProps !== newProps || hasLegacyContextChanged()) {
      // --- process when change occured ---
    } else if (updateExpirationTime < renderExpirationTime) {
      // --- process when there is no pending work ---
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  } else {
    didReceiveUpdate = false;
  }

  // Before entering the begin phase, clear the expiration time.
  workInProgress.expirationTime = NoWork;

  switch (workInProgress.tag) {
    // --- other cases ---

    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime);

    // --- other cases ---
  }
  // --- warning ---
}
  • updateHostRootの実行です。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberBeginWork.js#L838

// current              = root-HostRootFiber
// workInProgress       = root-workInProgress
// renderExpirationTime = root-expirationTime
function updateHostRoot(current, workInProgress, renderExpirationTime) {
  pushHostRootContext(workInProgress);
  const updateQueue = workInProgress.updateQueue;
  // --- warning ---
  const nextProps = workInProgress.pendingProps;
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState !== null ? prevState.element : null;
  processUpdateQueue(
    workInProgress,
    updateQueue,
    nextProps,
    null,
    renderExpirationTime,
  );
  const nextState = workInProgress.memoizedState;
  const nextChildren = nextState.element;
  // --- process when next/prev children are same ---
  const root: FiberRoot = workInProgress.stateNode;
  if (
    (current === null || current.child === null) &&
    root.hydrate &&
    enterHydrationState(workInProgress)
  ) {
    // --- other process ---
  } else {
    reconcileChildren(
      current,
      workInProgress,
      nextChildren,
      renderExpirationTime,
    );
    resetHydrationState();
  }
  // --- other process ---
}
  • processUpdateQueueを実行してうぷで!

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactUpdateQueue.js#L416

// workInProgress       = root-workInProgress
// queue     = root-workInProgress.updateQueue
// props                = null
// instance             = null
// renderExpirationTime = root-expirationTime
export function processUpdateQueue<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  props: any,
  instance: any,
  renderExpirationTime: ExpirationTime,
): void {
  hasForceUpdate = false;

  queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);

  // --- dev ---

  // These values may change as we process the queue.
  let newBaseState = queue.baseState;
  let newFirstUpdate = null;
  let newExpirationTime = NoWork;

  // Iterate through the list of updates to compute the result.
  let update = queue.firstUpdate;
  let resultState = newBaseState;
  while (update !== null) {
    const updateExpirationTime = update.expirationTime;
    if (updateExpirationTime < renderExpirationTime) {
      // --- process with insufficient expirationTime ---
    } else {
      resultState = getStateFromUpdate(
        workInProgress,
        queue,
        update,
        resultState,
        props,
        instance,
      );
      const callback = update.callback;
      if (callback !== null) {
        workInProgress.effectTag |= Callback;
        // Set this to null, in case it was mutated during an aborted render.
        update.nextEffect = null;
        if (queue.lastEffect === null) {
          queue.firstEffect = queue.lastEffect = update;
        } else {
          queue.lastEffect.nextEffect = update;
          queue.lastEffect = update;
        }
      }
    }
    // Continue to the next update.
    update = update.next;
  }

  // --- process when queue has capturedUpdate ---

  if (newFirstUpdate === null) {
    queue.lastUpdate = null;
  }
  if (newFirstCapturedUpdate === null) {
    queue.lastCapturedUpdate = null;
  } else {
    workInProgress.effectTag |= Callback;
  }
  if (newFirstUpdate === null && newFirstCapturedUpdate === null) {
    newBaseState = resultState;
  }

  queue.baseState = newBaseState;
  queue.firstUpdate = newFirstUpdate;
  queue.firstCapturedUpdate = newFirstCapturedUpdate;

  workInProgress.expirationTime = newExpirationTime;
  workInProgress.memoizedState = resultState;

  // --- dev ---
}
  • getStateFromUpdateです。今回のtagUpdateState
  • 今回は実質update.payloadを新しいオブジェクトに展開するだけです。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactUpdateQueue.js#L341

// workInProgress       = root-workInProgress
// queue                = root-workInProgress.updateQueue
// update               = root-update
// prevState            = null
// props                = null
// instance             = null
function getStateFromUpdate<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  update: Update<State>,
  prevState: State,
  nextProps: any,
  instance: any,
): any {
  switch (update.tag) {
    // --- other cases ---
    case UpdateState: {
      const payload = update.payload;
      let partialState;
      if (typeof payload === 'function') {
        // --- process when payload is function ---
      } else {
        partialState = payload;
      }
      if (partialState === null || partialState === undefined) {
        return prevState;
      }
      return Object.assign({}, prevState, partialState);
    }
    // --- other cases ---
  }
  return prevState;
}
// root-workInProgress
FiberNode {
  actualDuration: 0
  actualStartTime: 59901.585000000065
  alternate: root-HostRootFiber
  child: null
  childExpirationTime: 0
  contextDependencies: null
  effectTag: 32
  elementType: null
  expirationTime: 0
  firstEffect: null
  index: 0
  key: null
  lastEffect: null
  memoizedProps: null
  memoizedState: {element: {}}
  mode: 4
  nextEffect: null
  pendingProps: null
  ref: null
  return: null
  selfBaseDuration: 0
  sibling: null
  stateNode: root-internalRoot
  tag: 3
  treeBaseDuration: 0
  type: null
  updateQueue: {baseState: {}, firstUpdate: null, lastUpdate: null, firstCapturedUpdate: null, lastCapturedUpdate: null, }
}
// root-workInProgress.updateQueue
{
  baseState: {element: root-ReactElement}
  firstCapturedEffect: null
  firstCapturedUpdate: null
  firstEffect: root-update
  firstUpdate: null
  lastCapturedEffect: null
  lastCapturedUpdate: null
  lastEffect: root-update
  lastUpdate: null
}
  • reconcileChildrenの実行で、Childrenを良きようにします。
  • childのプロパティの中身は後ほど示します。

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberBeginWork.js#L151

// current              = root-HostRootFiber
// workInProgress       = root-workInProgress
// nextChildren         = root-ReactElement
// renderExpirationTime = root-expirationTime
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
) {
  if (current === null) {
    // --- other process ---
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}
  • reconcileChildFibers
  • このwrapperはなんか最適化のためらしいけどもどういうことか・・・

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactChildFiber.js#L1343

export const reconcileChildFibers = ChildReconciler(true);

function ChildReconciler(shouldTrackSideEffects) {
  // --- other functions ---

  function placeSingleChild(newFiber: Fiber): Fiber {
    // This is simpler for the single child case. We only need to do a
    // placement for inserting new children.
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.effectTag = Placement;
    }
    return newFiber;
  }

  function reconcileSingleElement () {}

  // --- other functions ---

  // returnFiber       = root-HostRootFiber
  // currentFirstChild = null
  // newChild          = root-ReactElement
  // expirationTime    = root-expirationTime
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

    // Handle object types
    const isObject = typeof newChild === 'object' && newChild !== null;

    if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
        // --- other case ---
      }
    }

    // --- other process ---
  }

  // --- other process ---
}
// returnFiber          = root-workInProgress
// currentFirstChild    = null
// element              = root-ReactElement
// renderExpirationTime = root-expirationTime
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  expirationTime: ExpirationTime,
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  while (child !== null) {
    // --- process when child is not null ---
  }

  if (element.type === REACT_FRAGMENT_TYPE) {
    // --- process when fragment ---
  } else {
    const created = createFiberFromElement(
      element,
      returnFiber.mode,
      expirationTime,
    );
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
}
  • createFiberFromElementを実行してFiberを生成します

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiber.js#L540

// element        = root-ReactElement
// mode           = 4
// expirationTime = root-expirationTime
export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  expirationTime: ExpirationTime,
): Fiber {
  let owner = null;
  // --- dev: process for debug ---
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    expirationTime,
  );
  // --- dev: process for debug ---
  return fiber;
}

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiber.js#L434

// type           = App
// key            = null
// pendingProps   = {}
// owner          = null
// mode           = 4
// expirationTime = root-expirationTime
export function createFiberFromTypeAndProps(
  type: any, // React$ElementType
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  expirationTime: ExpirationTime,
): Fiber {
  let fiber;

  let fiberTag = IndeterminateComponent;
  let resolvedType = type;
  if (typeof type === 'function') {
    if (shouldConstruct(type)) {
      // --- when its class Component ---
    }
  } else if (typeof type === 'string') {
    // --- when its HostComponent
  } else {
    // --- when else ---
  }

  fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.expirationTime = expirationTime;

  return fiber;
}
  • 生成されたFiberはこちらです。reconcileChildrenの返り値でroot-workInProgress.childの中身になります。
  • 以降root-childFiberと呼びます
// root-childFiber

FiberNode {
  actualDuration: 0
  actualStartTime: -1
  alternate: null
  child: null
  childExpirationTime: 0
  contextDependencies: null
  effectTag: 2
  elementType: ƒ App(props)
  expirationTime: 1073741823
  firstEffect: null
  index: 0
  key: null
  lastEffect: null
  memoizedProps: null
  memoizedState: null
  mode: 4
  nextEffect: null
  pendingProps: {}
  ref: null
  return: root-workInProgress
  selfBaseDuration: 0
  sibling: null
  stateNode: null
  tag: 2
  treeBaseDuration: 0
  type: ƒ App(props)
  updateQueue: null
}
  • call履歴をはるか遡って、workLoopの2週目まで戻ってきます。
// nextUnitOfWork = root-childFiber

// isYiedly = false
function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // --- other process ---
  }
}
  • 以降差分のみになります
// current              = null
// workInProgress       = root-childFiber
// renderExpirationTime = root-expirationTime
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  const updateExpirationTime = workInProgress.expirationTime;

  if (current !== null) {
    // --- process when not null ---
  } else {
    didReceiveUpdate = false;
  }

  workInProgress.expirationTime = NoWork;

  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      const elementType = workInProgress.elementType;
      return mountIndeterminateComponent(
        current,
        workInProgress,
        elementType,
        renderExpirationTime,
      );
    }
    // --- other cases ---
  }
}
  • mountIndeterminateComponentの実行です。
  • まずrenderWithHooksを実行してvalueの値を求めます。
  • その後再びのreconcileChildrenでchildrenの状態を求めます。

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-reconciler/src/ReactFiberBeginWork.js#L1128

// _current             = null
// workInProgress       = root-childFiber
// Component            = App
// renderExpirationTime = root-expirationTime
function mountIndeterminateComponent(
  _current,
  workInProgress,
  Component,
  renderExpirationTime,
) {
  if (_current !== null) {
    // --- process when current not null ---
  }

  const props = workInProgress.pendingProps;
  const unmaskedContext = getUnmaskedContext(workInProgress, Component, false);
  const context = getMaskedContext(workInProgress, unmaskedContext);

  prepareToReadContext(workInProgress, renderExpirationTime);

  let value;

  if (__DEV__) {
    // --- dev ---
  } else {
    value = renderWithHooks(
      null,
      workInProgress,
      Component,
      props,
      context,
      renderExpirationTime,
    );
  }
  workInProgress.effectTag |= PerformedWork;

  if (
    typeof value === 'object' &&
    value !== null &&
    typeof value.render === 'function' &&
    value.$$typeof === undefined
  ) {
    // --- process for class component ---
  } else {
    workInProgress.tag = FunctionComponent;
    // --- dev ---
    reconcileChildren(null, workInProgress, value, renderExpirationTime);
    // --- dev ---
    return workInProgress.child;
  }
}
  • renderWithHooksです。
  • なんと!ついにこちらで関数呼び出しが行われております!こういうのが見たかった
  • 中盤あたりで出てくるrenderedWorkはなんのために宣言されているのだろうか・・・

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-reconciler/src/ReactFiberHooks.js#L286

// current                  = null
// workInProgress           = root-childFiber
// Component                = App
// props                    = {}
// context                  = {}
// nextRenderExpirationTime = root-expirationTime
export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
  firstCurrentHook = nextCurrentHook =
    current !== null ? current.memoizedState : null;

  if (__DEV__) {
    // --- dev ---
  } else {
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

  let children = Component(props, refOrContext);

  if (didScheduleRenderPhaseUpdate) {
    do {
      // --- process when update ---
    } while (didScheduleRenderPhaseUpdate);

    renderPhaseUpdates = null;
    numberOfReRenders = 0;
  }

  // --- dev ---

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const renderedWork: Fiber = (currentlyRenderingFiber: any);

  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  renderedWork.effectTag |= sideEffectTag;

  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  firstCurrentHook = null;
  currentHook = null;
  nextCurrentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;
  nextWorkInProgressHook = null;

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;
  sideEffectTag = 0;

  // --- warning ---

  return children;
}
  • 生成されたchildrenはこちら
  • 以降root-AppChildrenReactElementと呼びます
// root-AppChildrenReactElement

{
  $$typeof: Symbol(react.element)
  key: null
  props: {children: "Hello world"}
  ref: null
  type: "div"
}
  • 再びreconcileChildrenの実行です。
// current              = null
// workInProgress       = root-childFiber
// nextChildren         = root-AppChildrenReactElement
// renderExpirationTime = root-expirationTime
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    // --- other process ---
  }
}
// root-childFiber.child

FiberNode {
  actualDuration: 0
  actualStartTime: -1
  alternate: null
  child: null
  childExpirationTime: 0
  contextDependencies: null
  effectTag: 0
  elementType: "div"
  expirationTime: 1073741823
  firstEffect: null
  index: 0
  key: null
  lastEffect: null
  memoizedProps: null
  memoizedState: null
  mode: 4
  nextEffect: null
  pendingProps: {children: "Hello world"}
  ref: null
  return: root-workInProgress
  selfBaseDuration: 0
  sibling: null
  stateNode: null
  tag: 5
  treeBaseDuration: 0
  type: "div"
  updateQueue: null
}
  • 3週目のworkLoopです。
// nextUnitOfWork = root-childFiber.child

function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // --- process when isYieldy ---
  }
}
// current              = null
// workInProgress       = root-childFiber.child
// renderExpirationTime = root-expirationTime
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  const updateExpirationTime = workInProgress.expirationTime;

  if (current !== null) {
    // --- other process ---
  } else {
    didReceiveUpdate = false;
  }

  workInProgress.expirationTime = NoWork;

  switch (workInProgress.tag) {
    // --- other cases ---
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderExpirationTime);
    // --- other cases ---
  }
}

https://github.com/facebook/react/blob/08e95543571eacbe88a03382adc9399607d53425/packages/react-reconciler/src/ReactFiberBeginWork.js#L911

// current              = null
// workInProgress       = root-childFiber.child
// renderExpirationTime = root-expirationTime
function updateHostComponent(current, workInProgress, renderExpirationTime) {
  pushHostContext(workInProgress);

  if (current === null) {
    tryToClaimNextHydratableInstance(workInProgress);
  }

  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;

  let nextChildren = nextProps.children;
  const isDirectTextChild = shouldSetTextContent(type, nextProps);

  if (isDirectTextChild) {
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    // --- process when content change ---
  }

  markRef(current, workInProgress);

  if (
    renderExpirationTime !== Never &&
    workInProgress.mode & ConcurrentMode &&
    shouldDeprioritizeSubtree(type, nextProps)
  ) {
    workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
    return null;
  }

  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}
  • 上記までを実行してworkInProgress.childの中身はnullになっています。
  • のでこの3週目のperformUnitOfWorkで初めてcompleteUnitOfWorkが実行されます。
// workInProgress = root-childFiber.child
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  // --- truncate ---

  let next;
  if (enableProfilerTimer) {
    // --- process ----
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
  }

  // --- dev ---

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }

  ReactCurrentOwner.current = null;

  return next;
}
2-2-2-1-1-3 completeUnitOfWork

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-reconciler/src/ReactFiberScheduler.js#L954

// workInProgress = root-childFiber.child
function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
  while (true) {
    const current = workInProgress.alternate;
    // --- dev ---

    const returnFiber = workInProgress.return;
    const siblingFiber = workInProgress.sibling;

    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      // --- dev ---
      nextUnitOfWork = workInProgress;
      if (enableProfilerTimer) {
        if (workInProgress.mode & ProfileMode) {
          startProfilerTimer(workInProgress);
        }
        nextUnitOfWork = completeWork(
          current,
          workInProgress,
          nextRenderExpirationTime,
        );
        if (workInProgress.mode & ProfileMode) {
          stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
        }
      } else {
        // --- other process ---
      }
      // --- dev ---
      stopWorkTimer(workInProgress);
      resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
      // --- dev ---

      if (nextUnitOfWork !== null) {
        return nextUnitOfWork;
      }

      if (
        returnFiber !== null &&
        (returnFiber.effectTag & Incomplete) === NoEffect
      ) {
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }
        if (workInProgress.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }
          returnFiber.lastEffect = workInProgress.lastEffect;
        }

        const effectTag = workInProgress.effectTag;
        if (effectTag > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
          } else {
            returnFiber.firstEffect = workInProgress;
          }
          returnFiber.lastEffect = workInProgress;
        }
      }

      // --- dev ---

      if (siblingFiber !== null) {
        // --- when sibling ---
      } else if (returnFiber !== null) {
        workInProgress = returnFiber;
        continue;
      } else {
        // --- when reached root ---
      }
    } else {
      // --- other process ---
    }
  }

  return null;
}
  • App関数内の<div>Hello world</div>向けのcompleteWorkの実行です
  • ReactFiberHostConfigというファイルから読み込まれた関数を実行しているのですが、そのファイルの実態はReactDOMのpackageに含まれていて、参照のためにrollupがごにょごにょと頑張るらしいです。

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-reconciler/src/ReactFiberCompleteWork.js#L539

// current              = null
// workInprogress       = root-childFiber.child
// renderExpirationTime = root-expirationTime
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    // --- other cases ---
    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        // --- other process ---
      } else {
        if (!newProps) {
          // --- process when without props
          break;
        }

        const currentHostContext = getHostContext();
        let wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          // --- process when hydrated ---
        } else {
          let instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );

          appendAllChildren(instance, workInProgress, false, false);

          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
          workInProgress.stateNode = instance;
        }

        if (workInProgress.ref !== null) {
          markRef(workInProgress);
        }
      }
      break;
    }
    // --- other cases ---
  }

  return null;
}
  • createInstanceです
  • precacheFiberNodeupdateFiberPropsの実行がこちらで有りますが、どうも実DOMのプロパティにfiberとpropsの参照を持たせているようです。これは知らなかった。どっかで使われるんだろうか

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-dom/src/client/ReactDOMHostConfig.js#L174

// type                   = "div"
// props                  = { children: "Hello world"}
// rootContainerInstance  = div#app-root
// hostContext            = {namespace: "http://www.w3.org/1999/xhtml", ancestorInfo: {…}}
// internalInstanceHandle = root-childFiber.child
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  if (__DEV__) {
    // --- dev ---
  } else {
    parentNamespace = ((hostContext: any): HostContextProd);
  }
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}
  • ついにdocument.createElementが実行されております。こちらにおりましたか。。
  • 生成されたdomElementは以降root-AppChildDOMと呼びます

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-dom/src/client/ReactDOMComponent.js#L374

// type = 'div'
// props = { children: "Hello world" }
// rootContainerElement = div#app-root
// parentNameSpace = "http://www.w3.org/1999/xhtml"
export function createElement(
  type: string,
  props: Object,
  rootContainerElement: Element | Document,
  parentNamespace: string,
): Element {
  let isCustomComponentTag;

  const ownerDocument: Document = getOwnerDocumentFromRootContainer(
    rootContainerElement,
  );
  let domElement: Element;
  let namespaceURI = parentNamespace;
  if (namespaceURI === HTML_NAMESPACE) {
    namespaceURI = getIntrinsicNamespace(type);
  }
  if (namespaceURI === HTML_NAMESPACE) {
    // --- dev: warning ---

    if (type === 'script') {
      // --- when type is script ---
    } else if (typeof props.is === 'string') {
      // --- when its web components? ---
    } else {
      domElement = ownerDocument.createElement(type);
      // --- process for select tag ---
    }
  } else {
    // --- process for namespaced element ---
  }

  if (__DEV__) {
    // --- dev: warning ---
  }

  return domElement;
}
  • completeWorkの次の実行に移りましてappendAllChildrenの実行です
  • 今回のworkInProgressはchildがnullなのでwhile内は実行しません

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-reconciler/src/ReactFiberCompleteWork.js#L103

// parent                = root-AppChildDOM
// workInProgress        = root-childFiber.child
// needsVisibilityToggle = false
// isHidden              = false
appendAllChildren = function(
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean,
  ) {
    let node = workInProgress.child;
    while (node !== null) {
      // --- process ---
    }
  };
  • completeWorkの次のfinalizeInitialChildrenの実行です

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-dom/src/client/ReactDOMHostConfig.js#L219

// domElement            = root-AppChildDOM
// type                  = "div"
// props                 = { children: "Hello world" }
// rootContainerInstance = div#app-root
// hostContext           = {namespace: "http://www.w3.org/1999/xhtml", ancestorInfo: {…}}
export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-dom/src/client/ReactDOMComponent.js#L468

// domElement            = root-AppChildDOM
// type                  = "div"
// props                 = { children: "Hello world" }
// rootContainerInstance = div#app-root
export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document,
): void {
  const isCustomComponentTag = isCustomComponent(tag, rawProps);
  // --- dev: warning ---

  let props: Object;
  switch (tag) {
    // --- other cases ---
    default:
      props = rawProps;
  }

  assertValidProps(tag, props);

  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );

  switch (tag) {
    // --- process input related tag ---
    default:
      if (typeof props.onClick === 'function') {
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      break;
  }
}

https://github.com/facebook/react/blob/f24a0da6e0f59484e5aafd0825bb1a6ed27d7182/packages/react-dom/src/client/ReactDOMComponent.js#L289

// tag                  = "div"
// domElement           = root-AppChildDOM
// rootContainerElement = div#app-root
// nextProps            = { children: "Hello world" }
// isCustomComponentTag = false
function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      // --- process when key is style ---
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // --- process when key is dangerouslySetInnerHtml ---
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);
        }
      } else if (typeof nextProp === 'number') {
        // --- process when props is number ---
      }
    } else if (
      // --- other cases ---
    }
  }
}
  • ここまできて、completeUnitOfWorkcompleteWorkの実行が終わり、その後何やかんやがあってcompleteUnitOfWorkのwhileをもう2週ほどするのですが、やんごとなき事情により割愛します
  • 呼び出し履歴をはるか戻ってrenderRootworkLoop呼び出し部分が完了してその続きです。
  • onCompleteを呼び出して、rootexpirationTimeの更新とfinishedWorkの更新をしています
// nextUnitOfWork = null

// root     = root-ReactRoot
// isYieldy = false
function renderRoot(root: FiberRoot, isYieldy: boolean): void {
  // --- previous process ---
  do {
    try {
      workLoop(isYieldy);
    } catch (thrownValue) {
      // --- recovery process ---
    }
    break;
  } while (true);

  // --- process when schedulerTracing ---

  isWorking = false;
  ReactCurrentDispatcher.current = previousDispatcher;
  resetContextDependences();
  resetHooks();

  // Yield back to main thread.
  if (didFatal) {
    // --- process when fatal ---
    return;
  }

  if (nextUnitOfWork !== null) {
    // --- process ---
    return;
  }

  const didCompleteRoot = true;
  stopWorkLoopTimer(interruptedBy, didCompleteRoot);
  const rootWorkInProgress = root.current.alternate;
  // --- warning ---

  nextRoot = null;
  interruptedBy = null;

  if (nextRenderDidError) {
    // --- process when error ---
  }

  if (isYieldy && nextLatestAbsoluteTimeoutMs !== -1) {
    // --- process when isYieldy ---
    return;
  }

  // Ready to commit.
  onComplete(root, rootWorkInProgress, expirationTime);
}
2-2-2-1-2 completeRoot
  • ついについにrenderRootの実行が終了しまして、performWorkOnRootに処理が戻ります
再喝
// root           = root-ReactRoot
// expirationTime = root-expirationTime
// isYieldy       = false
function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isYieldy: boolean,
) {
  // --- warning ---
  isRendering = true;

  if (!isYieldy) {
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // --- process ---
    } else {
      // --- previous process ---
      renderRoot(root, isYieldy);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        completeRoot(root, finishedWork, expirationTime);
      }
    }
  } else {
    // --- process when isYieldy ---
  }

  isRendering = false;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サーバーサイドでレンダリングされたHTMLを埋め込むReact SPAで、MathJaxでTypesetする

MathJax向けに書かれた数式を含むサーバーサイドでレンダリングされたHTMLを取ってきて、ReactのSPAに埋め込む場合、

componentDidMountでロード

async componentDidMount() {
  // この設定は、\colorを使うために必要
  ;(window as any).MathJax = {
    loader: {load: ['[tex]/color']},
    tex: {packages: {'[+]': ['color']}}
  }

  // MathJaxをロード
  const x = document.createElement('script')
  x.type = "text/javascript"
  x.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
  x.async = true
  ReactDOM.findDOMNode(this).appendChild(x)
}

componentDidUpdateでTypeset

componentDidUpdate() {
  const es = document.getElementsByClassName("math-expr")
  if (es) {
    ;(window as any).MathJax.typeset(es)
  }
}

\colorが使えない!

version 3から、autoloadの設定にするか、明示的にロードしないと使えなくなったようです。
http://docs.mathjax.org/en/latest/input/tex/extensions/color.html

また、書式も変わっています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】お問い合わせフォームを実装しよう

今回はReactを使って、下図のようなお問い合わせフォームを実装していきます。
真偽値を使ったフォームの変換と入力情報の取得、エラーメッセージを実装していきます。
Reactを使えばフォームの入力やボタンのクリックに応じてリアルタイムに表示を変えることができます。

完成図

お問い合わせ入力フォームがあり、

Image from Gyazo

送信すると、

Image from Gyazo

このように表示されているフォームを変換していきます。
また、入力がない場合にエラーメッセージが出力されるようにしましょう。

Image from Gyazo

雛形

完成コードになります。
要所で詳細に説明していきます。

ContactFrom.js
import React from 'react';

class ContactForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isSubmitted: false,
      email: "sample@gmail.com",
      hasEmailError: false,
      content: "お問い合わせ内容",
      hasContactError: false,
    };
  }

  handleSubmit() {
    this.setState({isSubmitted: true});
  }

  handleEmailChange(event) {
    const inputValue = event.target.value;
    const isEmpty = inputValue === "";
    this.state = {                      
      emial: inputValue,
      hasEmailError: isEmpty,
    }
  }

  handleContentChange(event) {
    const inputValue = event.target.value;
    const isEmpty = inputValue === "";
    this.state = {
      content: inputValue,
      hasContentError: isEmpty,
    }
  }

  render() {
    let emailErrorText;
    if (this.state.hasEmailError) {
      emailErrorText = (
        <span>
          emailを入力してください
        </span>    
      );
    }

    let contentErrorText;
    if (this.state.hascontentError) {
      contentErrorText = (
        <span>
          お問い合わせ内容を入力してください
        </span>
      );
    } 

    let contactForm;
    if (this.state.isSubmitted) {
      contactForm = (
        <span className = "message">送信完了しました<span>
      );
    } else {
      contactForm = (
        <form onSubmit={()=>{handleSubmit()}}>
          <p>メールアドレス(必須)</p>
          <input
            value = {this.state.email}
            onChange={(event)=>{handleEmailChange(event)}}
          />
          {emailErrorText}
          <p>お問い合わせ(必須)</p>
          <textarea 
            value = {this.state.content}
            onChange={(event)=>handleContenttChange(event)}
          />
          {contentErrortext}
          <input type="submit" value="送信" />  
        </form>
      );
    }

    return(
      <div className = "container">
        {contactForm}
     </div>
    );
  }

export default ContactForm;

}

送信ボタンで表示を切り替える

Image from Gyazo
           ↓
Image from Gyazo

stateを定義

stateでフォームが送信されたかどうかを管理していきます。
最初フォームは送信されていないため、isSubmittedの初期値はfalseです。

ContactForm.js
  constructor(props) {
    super(props);
    this.state = {
      isSubmitted: false,
    };
  }

stateの表示と条件分岐

stateの表示と表示切り替えの条件分岐を作っていきます。

ContactForm.js
    // 空の変数を準備
    let contactForm;
    // フォームが送信された場合の処理
    if (this.state.isSubmitted) {
      contactForm = (
        <span className = "message">送信完了しました<span>
      );
    } else {
      // stateの初期値はfalseなので以下のJSXが初期で表示されます
      contactForm = (
        <form onSubmit={()=>{handleSubmit()}}>
          <p>メールアドレス(必須)</p>
          <input
            value = {this.state.email}
            onChange={(event)=>{handleEmailChange(event)}}
          />
          {emailErrorText}
          <p>お問い合わせ(必須)</p>
          <textarea 
            value = {this.state.content}
            onChange={(event)=>handleContenttChange(event)}
          />
          {contentErrortext}
          <input type="submit" value="送信" />  
        </form>
      );
    }

    return(
      <div className = "container">
        // 変数contactFormを定義しstateを表示
        {contactForm}
     </div>
    );
  }

表示の切り替え

onSubmitイベントを使って、表示を切り替えていきましょう。
まずはsetStateを使ったhandleSubmitメソッドを作っていきます。

ContactForm.js
  handleSubmit() {
    this.setState({isSubmitted: true});
  }

次に<form>に対してonSubmitイベントを作っていきます。

ContactForm.js
    // 省略

    } else {
      contactForm = (
        // フォームを送信するとhandleSubmitメソッドを呼び出しstateが変更される
        <form onSubmit={()=>{handleSubmit()}}>
          <p>メールアドレス(必須)</p>

    // 省略

以上でお問い合わせフォームの切り替えの実装は終了です。
次にエラーメッセージの実装です。

入力情報の取得とエラーメッセージの表示

入力情報の取得かつ入力欄に何も内容がない場合にエラーメッセージを出力させましょう。

Image from Gyazo

stateの設定と表示

stateを設定します。

ContactFrom.js
  constructor(props) {
    super(props);
    this.state = {
      isSubmitted: false,
      // emailの初期値を設定します
      email: "sample@gmail.com",
      // 入力値が空かどうかの状態を管理します
      hasEmailError: false,
    };
  }

stateを表示します。
inputでstateを表示させる時はvalue属性に値を指定しましょう。

ContactFrom.js
    } else {
      contactForm = (
        <form onSubmit={()=>{handleSubmit()}}>
          <p>メールアドレス(必須)</p>
          <input
            // state.emailの初期値を表示
            value = {this.state.email}
            onChange={(event)=>{handleEmailChange(event)}}
          />
          // 省略
      );
    }

入力された値の取得

このままではstateの初期値は表示されたが、値の入力ができません。
フォームの入力や削除が行われたときに処理を実行するには、onChangeイベントを用います。
inputタグに対してonChangeイベントを指定しましょう。

ContactForm.js
    } else {
      contactForm = (
        <form onSubmit={()=>{handleSubmit()}}>
          <p>メールアドレス(必須)</p>
          <input
            value = {this.state.email}
            // onChangeイベントを使ってhandleEmailChangeメソッドを呼び出し値を更新します
            onChange={(event)=>{handleEmailChange(event)}}
          />
       // 省略
    }

stateの更新

① event.target.velueとすることで入力値を取得することができます。定数inputValueに代入します。
② isEmptyに空のinputValueを代入します。
③ emailの更新の値をinputValueとすることで入力値を取得できます。
④ 入力値が空の時、hasEmailErrorの値をtrueにします。

ContactFrom.js
  // 引数にeventを持たせる
  handleEmailChange(event) {
    const inputValue = event.target.value; 
    const isEmpty = inputValue === ""; 
    // 複数同時更新
    this.state = {                      
      emial: inputValue, 
      hasEmailError: isEmpty, 
    }
  }

エラーメッセージの表示

条件分岐を用いて、エラーメッセージの処理をしていきます。

ContactFrom.js
    // 空の変数を準備
    let emailErrorText;
    // hasEmailErrorの値が空の場合の処理
    if (this.state.hasEmailError) {
      emailErrorText = (
        <span>
          emailを入力してください
        </span>    
      );
    }

エラーメッセージを表示します。

ContactFrom.js
    } else {
      contactForm = (
        <form onSubmit={()=>{handleSubmit()}}>
          <p>メールアドレス(必須)</p>
          <input
            value = {this.state.email}
            onChange={(event)=>{handleEmailChange(event)}}
          />
          // 変数を定義しエラーメッセージを表示します
          {emailErrorText}
      // 省略
    }

お問い合わせ入力値の取得とエラーメッセージの表示は、
emailと同様の処理になるので割愛します。

以上で一連の実装は終わりになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 学習の備忘録 初日

初めてのReact

Udemyでフロントエンドの基礎的な講座でCSS(SCSS)
HTML
JS
を使ってある程度のページの作成方法の理解も進んだし、いよいよフロントエンドエンジニアデビュー?と思っていると、どうやら世間的にはフレームワークなりライブラリなりでのweb制作がメジャーのようです。なので、続けてReactの勉強をスタート。
VueとReact迷ったけど、ReactのがいろいろメジャーっぽいってことでReactを選びました。

当方の技術レベル

HTML,CSS(SCSS)はMDNのリファレンスあればある程度のことはできるレベル、JSは講師の方のコードを読んで理解、要素やパーツの組み換え程度で一から書くのはまだまだです、

今回の教材

日本一わかりやすいReact入門(トラハックさん)

なんたって日本一わかりやすいですから期待大です。
入門#1は受講済み、かんたんにReactの概要の説明。

学習メモ

入門#2よりいよいよ構文の説明など始まりました。
自身の備忘録としてメモって行きますね。

JSXとは

-Facebookが開発
-React公式ドキュメントはほぼJSXで記述

Reactでは業界標準

JSXの基礎文法]

1.Reactパッケージのインストールが必須
//.jsxファイル内の先頭で宣言
import React from "react"

2.HTMLとほぼ同じ記述(classはclassName)

const App = ()=>{
return(
<div id="hoge" className="fuga">
<h1>Hello,World</h1>
</div>
);
};

3.{}内に変数や関数を埋め込める

const foo ="<h1>Hello,World</h1>"
const App =() => {
return(
<div id="hoge"className="fuga">
{foo}
</div>
);

4.変数名などはキャメルケースで記述

5.空要素は閉じる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【技術書まとめ】りあクト! TypeScript で極める現場の React 開発

読んだ動機

著者の前著を読み、とてもわかりやすかったので次は実際の開発の際に必要なCSS設計やテストなどを学ぶために手に取った。

第1章 デバッグをもっと簡単に

  • Degugger for Chrome でのデバッグ方法について
  • React Dev Tools について
  • Redux Dev Tools について

第2章 コンポーネントのスタイル戦略

  • CSS Modules の説明
    • ハイフンは使えない
    • composesはSassでの@extends
  • CSS IN JS の歴史
  • Emotion の説明
    • 親にJSX Pragma を使ったら子でも使う
    • フラグメントは使用不可
    • ESLint の逃げ設定
'@typescript-eslint/no-unused-vars': [ 'error', { varsIgnorePattern: '[Rr]eact' } ],

第3章 スタイルガイドを作る

  • Storybook の説明

第4章 ユニットテストを書く

  • テストを書く理由
    • 設計者の意図通りに機能が実現されているかの確認
    • 新規に追加した全ての処理に破綻がないかの確認
    • 既存の機能を破壊していないかの確認
    • モジュラリティの確保
  • 実際にやること

    • ロジック部分のテスト
      • APIハンドラ
      • Redux-Saga
    • Storybook のスナップショットテスト
    • 正常系のE2Eテスト
  • JestでAPI部分をテストする

  • Redux Saga Test Plan の説明

  • スナップショットテスト

    • Emotionを使っていればハッシュ値が変わるのでCSS変更もわかる
      • CSS in JS の導入理由

第5章 E2Eテストを自動化する

Cypress の使い方

第6章 プロフェッショナル React の流儀

  • Reactの流儀
    1. UIをコンポーネントの階層構造に落とし込む
    2. Reactで静的なバージョンを作成する
    3. UI状態を表現する必要かつ十分なstateを決定する
    4. state をどこに配置すべきなのかを明確にする
    5. 逆方向のデータフローを追加する (Reduxを使うときは必要ない)
  • 実際の手順

    • 4.まで作る
    • どれを Container にするか決める
      • 親Containerが子Containerを呼ぶのは避ける
    • Presentational Component を Storybook にスタイルガイドとして登録
    • そのContainer が発行するAction とその ActionCreator をつくる
    • Reducerをつくる
    • API通信が必要ならAPIハンドラとそのユニットテストをつくる
      • モック通信によるテスト
    • Actionに対応したSagaをつくる
    • ReduxDevToolsから生テキストのActionをDispatchして動作確認する
    • ReduxSagaTestPlan を使ってSaga とReducer のユニットテストを書く
    • 最後にContainer Component を作成する
    • 仕上げにCypress でE2Eテストを書く
  • Store の構造はドメインモデル

    • ページごとだと重複値が出てきてしまうから
    • 正規化が可能なようにしておく
  • セッション情報はLocal Storage に持たせる

    • Redux Persist

読了後のまとめ

前著のReactそのものの中身からさらに進んで、実際の開発現場の流れを知れたことが何よりよかった。
次作も読もうと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React hooksを基礎から理解する (useMemo編)

React hooksとは

React 16.8 で追加された新機能です。
クラスを書かなくても、 stateなどのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。

useMemoとは

useMemoは値を保存するためのhookで、何回やっても結果が同じ場合の値などを保存(メモ化)し、そこから値を再取得します。
不要な再計算をスキップすることから、パフォーマンスの向上が期待出来ます。
useCallbackは関数自体をメモ化しますが、useMemoは関数の結果を保持します。

メモ化とは

メモ化とは同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすることです。
都度計算しなくて良くなることからパフォーマンスを向上が期待できます。

基本形

依存配列が空の場合

const sampleMemoFunc = () => {
  const memoResult = useMemo(() => hogeMemoFunc(), [])

  return <div>{memoResult}</div>
}

依存配列=[deps] へ空配列を渡すと何にも依存しないので、1回のみ実行。
つまり、依存関係が変わらない場合はキャッシュから値をとってくる。

依存配列に変数が入っている場合

props.nameが変わるたびに関数を再実行させたい場合は以下のように書きます。

const sampleMemoFunc = (props) => {
  const memoResult = useMemo(() => hogeMemoFunc(props.name), [prope.name])

  return <div>{memoResult}</div>
}

依存配列=[deps] へ変数を並べると、変数のどれかの値が変わった時にfuncを再実行する。
つまり、依存関係が変わった場合に再実行する。

サンプル

import React, {useMemo, useState} from 'react'

const UseMemo = () => {
  const [count01, setCount01] = useState(0)
  const [count02, setCount02] = useState(0)

  const result01 = () => setCount01(count01 + 1)
  const result02 = () => setCount02(count02 + 1)

  // const square = () => {
  //   let i = 0
  //   while (i < 2) i++
  //   return count02 * count02
  // }

  const square = useMemo(() => {
    let i = 0
    while (i < 200000000000) i++
    return count02 * count02
  }, [count02])

  return (
    <>
      <div>result01: {count01}</div>
      <div>result02: {count02}</div>
      {/* <div>square: {square()}</div> */}
      <div>square: {square}</div>
      <button onClick={result01}>increment</button>
      <button onClick={result02}>increment</button>
    </>
  )
}

export default UseMemo

square関数をuseMemoに代入しない場合

const square = () => {
  let i = 0
  while (i < 200000000000) i++
  return count02 * count02
}

return <div>square: {square()}</div>

square関数をuseMemoに代入しない場合、square関数の処理に関係ないはずのresult01ボタンを押した場合でも明らかに処理が重い。
count01はsquare関数の処理は通していないので関係無いはずだが、コンポーネントが再生成されたタイミングでsquare関数が実行されてしまうことが原因で、処理が重くなっている。

square関数をuseMemoに代入した場合

const square = useMemo(() => {
  let i = 0
  while (i < 200000000000) i++
  return count02 * count02
}, [count02])

return <div>square: {square}</div>

square関数をuseMemoへ代入した場合、result01ボタンを押した時に処理の重さは感じられなくなった。

square関数をuseMemoに代入し値を保持することで、依存配列であるcount02が更新されない限り、square関数の処理が実行されなくなったため、result01ボタンを押した場合の処理が軽くなった。

最後に

次回は useRef について書きたいと思います。

参考にさせていただいたサイト
https://reactjs.org/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む