- 投稿日:2020-07-20T20:02:46+09:00
[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()
で変更があったか判別できるので再レンダリングしてもらえます以上
- 投稿日:2020-07-20T14:02:46+09:00
Reactでhello worldするまでを追ってみた
Reactでhello worldされるまでのコードを追ってみました。
React.createElementとReactDOM.renderの流れを追いたいと思います。
間違っている箇所などコメントいただけるととても喜びます。前提
- 以下のような箇所は条件分岐されているような箇所は飛ばします。
__DEV__
やenableSchedulerTracing
変数で条件分岐されている- コメントアウトは削除します。
- 読みやすさのためにコードを省力している部分は、以下のようなコメントアウトをします。
// --- 省略した処理の説明 ---
使用したもの
- chromeのdevtoolsで確認しやすいようにsetTimeoutが挟まれてます。
index.dev.jsximport 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 ReactElementcreateElement
- まずは
React.createElement
実行していきます。createElement
は引数を元にごにょごにょしてパラメータを整えて、Elementの生成処理をReactElement
関数に任せ、その返り値をそのまま返します。- 今回渡す引数にはpropsもchildrenもないので、ReactElementに渡す引数は非常に寂しいものになりました。
ReactCurrentOwner.current
という奴が出てきましたが、こちらはFiberのあたりを読んでみないとよくわからなそうです。// 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
というスペシャルなプロパティを定義するだけです。// 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-2ReactDOM.render
- まずはrender関数の実行です。第一引数には前節で生成されたReactElementの
root-ReactElement
が渡されています。- 第3引数のcallbackは渡していないのでundefinedです。
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の処理をさらに進めていきます。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); }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
と呼びます。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
、中身の_internalRoot
をroot-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-ReactRoot
のrender
メソッドを呼び出して、いよいよReactをmountしていきます。unbatchedUpdates
という関数が出てきていますが、、今回のケースでは渡された関数をそのまま実行するだけです。ReactWork
のインスタンスも生成して、その_onCommit
メソッドをupdateContainer
に渡しています。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#L162export 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するんでしょうenqueueUpdate
でcurrent
のFiberにupdate
を盛るscheduleWork
でFiber
のupdate
を実行する- と思われます
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
を呼び出して、fiber
のupdateQueue
に先ほど生成したupdate
をappendUpdateToQueue
によって盛り盛りします。root-HostRootFiber
は生成されたばかりでしたので、updateQueue
はここでcreateUpdateQueue
によって作られます。// 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がもつ
expirationTime
をrequestWork
に渡します。- if文で出てくる
nextRoot
はこのファイルが持つグローバル変数です。// 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-HostRootFiber
のreturn
プロパティがnull
かつtag
にHostRoot
の値を持っていますので、このfiber
のもつstateNode
(root-internalRoot
)をFiberRoot
ということにしてこの関数の実行を終了します。// 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
にも同様の数値が入っています。// 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
の実行です。ファイルのグローバル変数がごにょごにょされます。// 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#L2244function 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
を実行してnextFlushedRoot
とnextFlushedExpirationTime
のお世話をします- 実行後の値は以下です
nextFlushedRoot = root-ReactRoot nextFlushedExpirationTime = root-expirationTime
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 { 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
がほぼ本体です。// 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週します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
にあたります。// 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
の実行です。// 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
を実行してうぷで!// 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
です。今回のtag
はUpdateState
。- 今回は実質
update.payload
を新しいオブジェクトに展開するだけです。// 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
のプロパティの中身は後ほど示します。// 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はなんか最適化のためらしいけどもどういうことか・・・
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
を生成します// 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; }// 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の状態を求めます。// _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
はなんのために宣言されているのだろうか・・・// 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 --- } }// 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
// 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がごにょごにょと頑張るらしいです。// 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
ですprecacheFiberNode
とupdateFiberProps
の実行がこちらで有りますが、どうも実DOMのプロパティにfiberとpropsの参照を持たせているようです。これは知らなかった。どっかで使われるんだろうか// 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
と呼びます// 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内は実行しません// 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
の実行です// 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); }// 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; } }// 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 --- } } }
- ここまできて、
completeUnitOfWork
のcompleteWork
の実行が終わり、その後何やかんやがあってcompleteUnitOfWork
のwhileをもう2週ほどするのですが、やんごとなき事情により割愛します- 呼び出し履歴をはるか戻って
renderRoot
のworkLoop
呼び出し部分が完了してその続きです。onComplete
を呼び出して、root
のexpirationTime
の更新と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; }
- 投稿日:2020-07-20T13:42:29+09:00
サーバーサイドでレンダリングされた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また、書式も変わっています。
- 投稿日:2020-07-20T12:04:23+09:00
【React】お問い合わせフォームを実装しよう
今回はReactを使って、下図のようなお問い合わせフォームを実装していきます。
真偽値を使ったフォームの変換と入力情報の取得、エラーメッセージを実装していきます。
Reactを使えばフォームの入力やボタンのクリックに応じてリアルタイムに表示を変えることができます。完成図
お問い合わせ入力フォームがあり、
送信すると、
このように表示されているフォームを変換していきます。
また、入力がない場合にエラーメッセージが出力されるようにしましょう。雛形
完成コードになります。
要所で詳細に説明していきます。ContactFrom.jsimport 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; }送信ボタンで表示を切り替える
stateを定義
stateでフォームが送信されたかどうかを管理していきます。
最初フォームは送信されていないため、isSubmittedの初期値はfalseです。ContactForm.jsconstructor(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.jshandleSubmit() { this.setState({isSubmitted: true}); }次に<form>に対してonSubmitイベントを作っていきます。
ContactForm.js// 省略 } else { contactForm = ( // フォームを送信するとhandleSubmitメソッドを呼び出しstateが変更される <form onSubmit={()=>{handleSubmit()}}> <p>メールアドレス(必須)</p> // 省略以上でお問い合わせフォームの切り替えの実装は終了です。
次にエラーメッセージの実装です。入力情報の取得とエラーメッセージの表示
入力情報の取得かつ入力欄に何も内容がない場合にエラーメッセージを出力させましょう。
stateの設定と表示
stateを設定します。
ContactFrom.jsconstructor(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と同様の処理になるので割愛します。以上で一連の実装は終わりになります。
- 投稿日:2020-07-20T09:08:32+09:00
React 学習の備忘録 初日
初めてのReact
Udemyでフロントエンドの基礎的な講座でCSS(SCSS)
HTML
JS
を使ってある程度のページの作成方法の理解も進んだし、いよいよフロントエンドエンジニアデビュー?と思っていると、どうやら世間的にはフレームワークなりライブラリなりでのweb制作がメジャーのようです。なので、続けてReactの勉強をスタート。
VueとReact迷ったけど、ReactのがいろいろメジャーっぽいってことでReactを選びました。当方の技術レベル
HTML,CSS(SCSS)はMDNのリファレンスあればある程度のことはできるレベル、JSは講師の方のコードを読んで理解、要素やパーツの組み換え程度で一から書くのはまだまだです、
今回の教材
なんたって日本一わかりやすいですから期待大です。
入門#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.空要素は閉じる。
- 投稿日:2020-07-20T08:58:02+09:00
【技術書まとめ】りあクト! 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の流儀
- UIをコンポーネントの階層構造に落とし込む
- Reactで静的なバージョンを作成する
- UI状態を表現する必要かつ十分なstateを決定する
- state をどこに配置すべきなのかを明確にする
- 逆方向のデータフローを追加する (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そのものの中身からさらに進んで、実際の開発現場の流れを知れたことが何よりよかった。
次作も読もうと思う。
- 投稿日:2020-07-20T00:57:47+09:00
React hooksを基礎から理解する (useMemo編)
React hooksとは
React 16.8 で追加された新機能です。
クラスを書かなくても、state
などのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。
- React hooksを基礎から理解する (useState編)
- React hooksを基礎から理解する (useEffect編)
- React hooksを基礎から理解する (useContext編)
- React hooksを基礎から理解する (useReducer編)
- React hooksを基礎から理解する (useCallback編)
- React hooksを基礎から理解する (useMemo編)
今ここ
- React hooksを基礎から理解する (useRef編)
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 UseMemosquare関数を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/