Recoilize is a Chrome Dev Tool meant for debugging applications built with the experimental Recoil.js state management library.
The tool records Recoil state and allows users to easily debug their applications with features such as: time travel to previous states, visualization of the component graph and display of the atom selector network.
Please note that Recoilize is in BETA. We will continue to make improvements and implement fixes but if you find any issues, please dont hesitate to report them in the issues tab or submit a PR and we'll happily take a look.
Installation
#### Install Recoilize Module
```js
npm install recoilize
```
### ** IMPORTANT **
#### Import RecoilizeDebugger from the Recoilize module
```js
import RecoilizeDebugger from 'recoilize';
```
#### Integrate RecoilizeDebugger as a React component within the recoil root:
```js
import RecoilizeDebugger from 'recoilize';
import RecoilRoot from 'recoil';
ReactDOM.render(
,
document.getElementById('root'),
);
```
#### Please note, Recoilize assumes that the HTML element used to inject your React application has an ID of 'root'. If it does not the HTML element must be passed in as an attribute called 'root' to the RecoilizeDebugger component
#### Example:
```js
import RecoilizeDebugger from 'recoilize';
import RecoilRoot from 'recoil';
//If your app injects on an element with ID of 'app'
const app = document.getElementById('app');
ReactDOM.render(
,
app,
);
```
### In order to integrate Next.js applications with RecoilizeDebugger, follow the example below.
```js
//If your application uses Next.js modify the _app.js as follows
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
import { RecoilRoot } from 'recoil';
function MyApp({ Component, pageProps }) {
const [root, setRoot] = useState(null)
const RecoilizeDebugger = dynamic(
() => {
return import('recoilize');
},
{ ssr: false}
);
useEffect(() => {
if (typeof window.document !== 'undefined') {
setRoot(document.getElementById('__next'));
}
}, [root]);
return (
<>
>
);
}
export default MyApp;
```
#### Open your application on the Chrome Browser and start debugging with Recoilize!
##### (Only supported with React applications using Recoil as state management)
New Features for Version 3.0.0
Support for Recoil 0.1.3
Recoilize now supports the most recent update to the Recoil library and is backwards compatible with older versions of Recoil.
Time Travel with ease
If you had used Recoilize before, you would have noticed an annoying bug that sometimes breaks the app and won’t allow you to be productive. With the new version of Recoilize, that issue is forever gone. Users can now use the tool with confidence and worry-free.
The main mission of Recoilize 3.0 is to make it more user-friendly, so you will enjoy our brand new time travel feature — the time travel slider! Why click and scroll through snapshots when you can do it with a slider and some buttons, right?
Customizable Component Graph
This is one of the coolest updates of Recoilize 3.0. We understand that different users have different ways of thinking and visualizing, and for that reason, the component tree now is fully customizable. You can expand or collapse the components, choose vertical or horizontal displays or adjust the spacing between elements.
Better User Experience with Atom Network
The atom network is one of the key features that differentiate Recoil.js from other alternative state management libraries. However, the atom network will grow bigger together with the app. At some points, it will be unmanageable and hard to keep track of all of the atoms. To make this easier and pain-free, the new Atom Network will allow you to freely move and arrange them anywhere you want.
Snapshot Comparison
We understand that developers always develop an app with an optimization philosophy in mind. Component rendering time can be difficult to measure for two reasons. First, your computer and your web browser are not the same as mine, so the run-time can be vastly different. Second, it’s really hard to keep track of a long series of snapshots. You definitely don’t want to waste all of your time calculating the rendering time by hand.
With the new Recoilize, users can now save a series of state snapshots and use it later to analyze/compare with the current series.
Features
Support for Concurrent Mode
If a Suspense component was used as a placeholder during component renderings, those suspense components will display with a red border in the expanded component graph. This indicates that a component was suspended during the render of the selected snapshot.
Performance Metrics
In 'Metrics' tab, two graphs display component render times.
The flame graph displays the time a component took to render itself, and all of its child components. The bar graph displays the individual render times of each component.
Throttle
In the settings tab, users are able to set throttle (in milliseconds) for large scale applications or any applications that changes state rapidly. The default is set at 70ms.
State Persistence
Recoilize allows the users to persist their application's state through a refresh or reload. At this time, the user is able to view the previous states in the dev tool, but cannot time travel to the states before refresh.
Additional Features
legend to see relationship between component graph and state
toggle to view raw component graph
filter atom/selector network relationship
filter snapshots by atom/selector keys
We will continue updating Recoilize alongside Recoil's updates!
Recoilize는 Recoil 상태관리 라이브러리를 사용하여 만들어진 애플리케이션을 디버깅 할수있는 Chrome Dev Tool입니다.
Recoil 상태를 기록하여 유저들이 애플리케이션을 편하게 디버깅 할수 있도록 도와주는 기능들을 가지고 있습니다. 리액트 컴포넌트를 시각화 하여 그래프로 보여줌과 동시에 스냅샷을 이요하여 이전 상태로 시간이동을 가능하게 만들어줄수있는 도구입니다.
Recoilize는 현재 베타버젼 입니다. 툴을 계속 개선하고 새로운 이슈들을 수정해 나갈것이고, 혹시 다른 버그들이나 이슈들이 나타난다면 언제든지 이슈 탭에 글을 작성하시거나 PR을 해주시면 감사하겠습니다.
설치 방법
#### Recoilize 모듈 설치
```js
npm install recoilize
```
### ** 중요 **
#### Recoilize모듈에서 RecoilizeDebugger를 import해줘야 합니다
```js
import RecoilizeDebugger from 'recoilize';
```
#### RecoilizeDebugger를 recoil root 안에 리액트 컴포넌트로 넣어야 합니다
```js
import RecoilizeDebugger from 'recoilize';
import RecoilRoot from 'recoil';
ReactDOM.render(
,
document.getElementById('root'),
);
```
#### Recoilize는 리액트 애플리케이션을 주입시키기 위해 쓴 HTML 엘리먼트의 아이디를 'root'으로 가정합니다.아닐경우 RecoilizeDebugger에 'root'속성을 만들고 HTML 엘리먼트를 패스하십시오.
```js
import RecoilizeDebugger from 'recoilize';
import RecoilRoot from 'recoil';
//If your app injects on an element with ID of 'app'
const app = document.getElementById('app');
ReactDOM.render(
,
app,
);
```
#### 애플리케이션을 크롬 브라우저에서 열고 Recoilize 디버깅툴을 실행하시면 됩니다.
##### (현재 Recoil을 상태관리 라이브러리로 사용하는 리액트 애플리케이션만 지원합니다.)
새로운 기능
Recoil 0.1.2를 지원합니다
Recoilize는 최신 버전과 구버전의 Recoil과 호환이 됩니다
스냅샷 클리어
Previous와 Forward 버튼을 넣어 선택된 스냅샵의 전이나 후에 있는 스냅샷을 지울 수 있게 했습니다
컴포넌트 그래프
호버
그래프의 노드를 호버했을 때 안의 텍스트가 보이는 형태를 개선하였습니다
atom 범례
범례의 텍스트가 클릭되면 드롭다운 형태의 atom이나 selector 리스트가 보이게 하였습니다
드롭다운 리스트에 있는 각각의 atom이나 selector를 누를 경우 해당 atom이나 selector를 쓰는 컴포넌트가 하이라이트되도록 바꾸었습니다
atom 네트워크
atom 범례
범례의 텍스트가 클릭되면 드롭다운 형태의 atom이나 selector 리스트가 보이게 하였습니다
드롭다운 리스트에 있는 각각의 atom이나 selector를 누를 경우 관련 atom이나 selector 노드가 보이도록 했습니다
그래프
여러개의 그래프가 겹치지 않도록 조정하였습니다
검색 창
검색 창이 탐색 버튼과 겹치지 않도록 변경하였습니다
Ranked 그래프
애니메이션을 없애서 전과 후 상태비교가 쉽게 보이도록 바꾸었습니다
기능
Concurrent 모드 지원
만약 컴포넌트를 보류시키기 위해 Suspense 컴포넌트가 사용됐을 경우, 해당 컴포넌트의 노드 주위에 빨간 테두리로 표시하여 컴포넌트가 나타나기까지 지연되었음을 알려줄 것입니다
퍼포먼스 측정 그래프
'Metrics' 탭에 있는 두가지 그래프는 렌더링 시간을 보여줍니다
Flame 그래프는 각각의 컴포넌트와 자식 컴포넌트가 나타나기까지 걸린 합산된 시간을 보여주고 Ranked 그래프는 각각의 컴포넌트가 나오기까지 걸린 시간을 보여줍니다
시간 이동
Recoilize의 주요 기능 중 하나로, 이 도구는 사용자가 이전의 모든 스냅샷으로 이동할 수 있게 해줍니다. 각 스냅샷 옆에 있는 점프 버튼을 누르면 해당 스냅샷으로 상태를 설정하여 DOM이 변경됩니다.
시각화
사용자는 개별 스냅샷을 클릭하여 애플리케이션 상태에 대한 시각화된 그래프를 볼수있고, 컴포넌트 트리와 다른 그래프 뿐만 아니라 State tree를 JSON 형식으로 지원합니다
쓰로틀링
대규모 애플리케이션 또는 상태를 빠르게 변경하는 모든 애플리케이션에 대해 쓰로틀링(ms)을 설정할 수 있습니다. 기본값은 70ms로 설정되어 있습니다.
상태 유지 (베타)
Recoilize는 사용자가 새로 고침을 했을 경우에도 응용 프로그램의 상태를 유지할 수 있도록 해줍니다. 이때 사용자는 개발 도구에서는 이전 상태를 볼 수 있지만, 새로고침 전에 상태로의 시간 이동은 할 수 없습니다. 우리 팀은 여전히 이 기능을 완성하기 위해 노력하고 있습니다.
부가 기능
컴포넌트 그래프에 마우스를 올렸을때 관련있는 atom과 selector들이 나타납니다
컴포넌트 그래프 안에 오른쪽 작은 창에서 관련된 상태들을 선택하여 볼 있습니다
컴포넌트 그래프 안에 Expand 버튼을 누르면 확장된 컴포넌트 그래프를 볼 수 있습니다
네트워크 그래프 안에 atom과 selector들을 볼수있고 필터링도 가능합니다.
설정탭에서 atom과 selector key를 사용하여 관련된 스냅샷들을 필터링 할 수 있습니다
Recoilize is a Chrome Dev Tool meant for debugging applications built with the experimental Recoil.js state management library.
The tool records Recoil state and allows users to easily debug their applications with features such as time travel to previous states, visualization of the component graph and display of the atom selector network.
Please note that Recoilize is in BETA. We will continue to make improvements and implement fixes but if you find any issues, please dont hesitate to report them in the issues tab or submit a PR and we'll happily take a look.
Installation
#### Install Recoilize Module
```js
npm install recoilize
```
### ** IMPORTANT **
#### Import RecoilizeDebugger from the Recoilize module
```js
import RecoilizeDebugger from 'recoilize';
```
#### Integrate RecoilizeDebugger as a React component within the recoil root:
```js
import RecoilizeDebugger from 'recoilize';
import RecoilRoot from 'recoil';
ReactDOM.render(
,
document.getElementById('root'),
);
```
#### Please note, Recoilize assumes that the HTML element used to inject your React application has an ID of 'root'. If it does not the HTML element must be passed in as an attribute called 'root' to the RecoilizeDebugger component
#### Example:
```js
import RecoilizeDebugger from 'recoilize';
import RecoilRoot from 'recoil';
//If your app injects on an element with ID of 'app'
const app = document.getElementById('app');
ReactDOM.render(
,
app,
);
```
### In order to integrate Next.js applications with RecoilizeDebugger, follow the example below.
```js
//If your application uses Next.js modify the _app.js as follows
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
import { RecoilRoot } from 'recoil';
function MyApp({ Component, pageProps }) {
const [root, setRoot] = useState(null)
const RecoilizeDebugger = dynamic(
() => {
return import('recoilize');
},
{ ssr: false}
);
useEffect(() => {
if (typeof window.document !== 'undefined') {
setRoot(document.getElementById('__next'));
}
}, [root]);
return (
<>
>
);
}
export default MyApp;
```
#### Open your application on the Chrome Browser and start debugging with Recoilize!
##### (Only supported with React applications using Recoil as state management)
New Features for Version 2.0.1
Support for Recoil 0.1.3
Recoilize now supports the most recent update to the Recoil library.
Ease of Use
Recoilize nolonger requires atoms and selectors or the root HTML element to be passed into the RecoilizeDebugger React component. Simply import RecoilizeDubugger and integrate it within your app's RecoilRoot component.
Support for Concurrent Mode
Additonal functionality has been added for apps that utilize React's Suspense component. If a Suspense component was used to suspend component renderings those components will display with a red border in the component graph. This indicates that a component was suspended during the render of the selected snapshot.
Performance Metrics
A new tab, 'Metrics', has been incorperated into the dev tool. In this tab the user will find two graphs which display component render times.
The flame graph displays the time a component took to render itself, and all of its child components. The bar graph displays the individual render times of each component.
Features
Time Travel
As one of the key features of Recoilize, the tool enables users to jump to any previous snapshots. Pressing the jump button next to each of the snapshots will change the DOM by setting the state to that snapshot.
Visualizations
Users are able to view visualizations for their application's state by clicking individual snapshots. Recoilize provides component trees and graphs, as well as the state trees in JSON format.
Throttle
In the settings tab, users are able to set throttle (in milliseconds) for large scale applications or any applications that changes state rapidly. The default is set at 70ms.
State Persistence
Recoilize allows the users to persist their application's state through a refresh or reload. At this time, the user is able to view the previous states in the dev tool, but cannot time travel to the states before refresh.
Additional Features
component graph hover to view atoms and selectors
legend to see relationship between component graph and state
Toggle to view raw component graph
filter atom/selector network relationship
filter snapshots by atom/selector keys
We will continue updating Recoilize alongside Recoil's updates!
================================================
FILE: package/formatFiberNodes.js
================================================
// node parameter should be root of the fiber node tree, can be grapped with startNode from below
// const startNode = document.getElementById('root')._reactRootContainer._internalRoot.current;
const formatFiberNodes = node => {
const formattedNode = {
// this function grabs a 'name' based on the tag of the node
name: assignName(node),
tag: node.tag,
children: [],
recoilNodes: createAtomsSelectorArray(node),
actualDuration: node.actualDuration,
treeBaseDuration: node.treeBaseDuration,
wasSuspended: node.return && node.return.tag === 13 ? true : false,
};
// loop through and recursively call all nodes to format their 'sibling' and 'child' properties to our desired tree shape
let currentNode = node.child;
while (currentNode) {
formattedNode.children.push(formatFiberNodes(currentNode));
currentNode = currentNode.sibling;
}
return formattedNode;
};
const createAtomsSelectorArray = node => {
// initialize empty array for all atoms and selectors. Elements will be all atom and selector names, as strings
const recoilNodes = [];
//start the pointer at node.memoizedState. All nodes should have this key.
let currentNode = node.memoizedState;
// Traverse through the memoizedStates and look for the deps key which holds selectors or state.
while (currentNode) {
// if the memoizedState has a deps key, and that deps key is an array
// then the first value of that array will be an atom or selector
if (
typeof(currentNode) === 'object' &&
currentNode.hasOwnProperty('memoizedState') &&
typeof currentNode.memoizedState === 'object' &&
currentNode.memoizedState !== null &&
!Array.isArray(currentNode.memoizedState) &&
currentNode.memoizedState.hasOwnProperty('deps')
) {
if (
Array.isArray(currentNode.memoizedState.deps) &&
typeof currentNode.memoizedState.deps[0] === 'object' &&
currentNode.memoizedState.deps[0] !== null
) {
// if recoilNodes (arr) includes the current atom or selector
if (!recoilNodes.includes(currentNode.memoizedState.deps[0].key)) {
// otherwise push atom/selector to recoilNodes
recoilNodes.push(currentNode.memoizedState.deps[0].key);
}
}
}
// move onto next node
currentNode = currentNode.next;
}
// return atom and selectors array
return recoilNodes;
};
// keep an eye on this section as we test bigger and bigger applications SEAN
const assignName = node => {
// Returns symbol key if $$typeof is defined. Some components, such as context providers, will have this value.
if (node.type && node.type.$$typeof) return Symbol.keyFor(node.type.$$typeof);
// Return suspense if tag is equal to 13, which is associated with Suspense components.
if (node.tag === 13) return 'Suspense';
// Find name of a class component
if (node.type && node.type.name) return node.type.name;
// Tag 5 === HostComponent
if (node.tag === 5) return `${node.type}`;
// Tag 3 === HostRoot
if (node.tag === 3) return 'HR';
// Tag 6 === HostText
if (node.tag === 6) return node.memoizedProps;
// Tag 7 === Fragment
if (node.tag === 7) return 'Fragment';
};
module.exports = { formatFiberNodes };
// if testing this function on the browser, use line below to log the formatted tree in the console
//let formattedFiberNodes = formatFiberNodes(document.getElementById('root')._reactRootContainer._internalRoot.current)
================================================
FILE: package/formatFiberNodes.ts
================================================
// node parameter should be root of the fiber node tree, can be grapped with startNode from below
// const startNode = document.getElementById('root')._reactRootContainer._internalRoot.current;
type node = {
tag: number;
key: any;
elementType: string;
child: any;
sibling: any;
actualDuration: number;
treeBaseDuration: number;
return: any;
};
type formattedNode = {
name: string;
tag: number;
children: any[];
recoilNodes: any[];
// adding in render time for fiber node
actualDuration: number;
treeBaseDuration: number;
wasSuspended: boolean;
};
const formatFiberNodes = (node: node) => {
const formattedNode: formattedNode = {
actualDuration: node.actualDuration,
treeBaseDuration: node.treeBaseDuration,
// this function grabs a 'name' based on the tag of the node
name: assignName(node),
tag: node.tag,
children: [],
recoilNodes: createAtomsSelectorArray(node),
wasSuspended: node.return && node.return.tag === 13 ? true : false,
};
// loop through and recursively call all nodes to format their 'sibling' and 'child' properties to our desired tree shape
let currentNode = node.child;
while (currentNode) {
formattedNode.children.push(formatFiberNodes(currentNode));
currentNode = currentNode.sibling;
}
return formattedNode;
};
const createAtomsSelectorArray = (node: any) => {
// initialize empty array for all atoms and selectors. Elements will be all atom and selector names, as strings
const recoilNodes = [];
//start the pointer at node.memoizedState. All nodes should have this key.
let currentNode = node.memoizedState;
// Traverse through the memoizedStates and look for the deps key which holds selectors or state.
while (currentNode) {
// if the memoizedState has a deps key, and that deps key is an array of length 2 then the first value of that array will be an atom or selector
if (
currentNode.deps &&
Array.isArray(currentNode.deps) &&
currentNode.deps.length === 2
) {
// if the atom/selector already exist in the recoilNodes array then break from this while loop. At this point you are traversing through previous atom/selector deps.
if (recoilNodes.includes(currentNode.deps[0].key)) break;
recoilNodes.push(currentNode.deps[0].key);
// if an atom/selector was successfully pushed into the recoilNodes array then the pointer should now point to the next key, which will have its own deps key if there is another atom/selector
currentNode = currentNode.next;
} else {
// This is the case where there is no atom/selector in the memoizedState. Look into the memoized state of the next key. If that doesn't exist then break from the while loop because there are no atoms/selectors at this point.
if (!currentNode.next) break;
if (!currentNode.next.memoizedState) break;
currentNode = currentNode.next.memoizedState;
}
}
return recoilNodes;
};
// keep an eye on this section as we test bigger and bigger applications
const assignName = (node: any) => {
// Returns symbol key if $$typeof is defined. Some components, such as context providers, will have this value.
if (node.type && node.type.$$typeof) return Symbol.keyFor(node.type.$$typeof);
// Return suspense if tag is equal to 13, which is associated with Suspense components.
if (node.tag === 13) return 'Suspense';
// Find name of a class component
if (node.type && node.type.name) return node.type.name;
// Tag 5 === HostComponent
if (node.tag === 5) return `${node.type}`;
// Tag 3 === HostRoot
if (node.tag === 3) return 'HR';
// Tag 3 === HostText
if (node.tag === 6) {
return node.memoizedProps;
}
if (node.tag === 7) return 'Fragment';
};
export default formatFiberNodes;
// if testing this function on the browser, use line below to log the formatted tree in the console
//let formattedFiberNodes = formatFiberNodes(document.getElementById('root')._reactRootContainer._internalRoot.current)
================================================
FILE: package/index.js
================================================
import React, {useState, useEffect} from 'react';
import {
useRecoilTransactionObserver_UNSTABLE,
useRecoilSnapshot,
useGotoRecoilSnapshot,
useRecoilState,
useGetRecoilValueInfo_UNSTABLE
} from 'recoil';
import {formatFiberNodes} from './formatFiberNodes';
// grabs isPersistedState from sessionStorage
let isPersistedState = sessionStorage.getItem('isPersistedState');
// isRestored state disables snapshots from being recorded
// when we jump backwards
let isRestoredState = false;
// set default throttle to 70, throttle timer changes with every snapshot
let throttleTimer = 0;
let throttleLimit = 70;
// assign the value of selectorsObject in formatRecoilizeSelectors function
// will contain the selectors from a user application
let selectorsObject;
export default function RecoilizeDebugger(props) {
// We should ask for Array of atoms and selectors.
// Captures all atoms that were defined to get the initial state
// Define a recoilizeRoot variable which will be assigned based on whether a root is passed in as a prop
let recoilizeRoot;
// Check if a root was passed to props.
if (props.root) {
const {root} = props;
recoilizeRoot = root;
} else {
recoilizeRoot = document.getElementById('root');
}
const snapshot = useRecoilSnapshot();
// getNodes_UNSTABLE will return an iterable that contains atom and selector objects.
const nodes = [...snapshot.getNodes_UNSTABLE()];
// Local state of all previous snapshots to use for time traveling when requested by dev tools.
const [snapshots, setSnapshots] = useState([snapshot]);
// const [isRestoredState, setRestoredState] = useState(false);
const gotoSnapshot = useGotoRecoilSnapshot();
const filteredSnapshot = {};
/*
A nodeDeps object is constructed using getDeps_UNSTABLE.
This object will then be used to construct a nodeSubscriptions object.
After continuous testing, getSubscriptions_UNSTABLE was deemed too unreliable.
*/
const nodeDeps = {};
const nodeSubscriptions = {};
nodes.forEach(node => {
const getDeps = [...snapshot.getInfo_UNSTABLE(node).deps];
nodeDeps[node.key] = getDeps.map(dep => dep.key);
});
for (let key in nodeDeps) {
nodeDeps[key].forEach(node => {
if (nodeSubscriptions[node]) {
nodeSubscriptions[node].push(key);
} else {
nodeSubscriptions[node] = [key];
}
});
}
// Traverse all atoms and selector state nodes and get value
nodes.forEach((node, index) => {
const type = node.__proto__.constructor.name;
const contents = snapshot.getLoadable(node).contents;
// Construct node data structure for dev tool to consume
filteredSnapshot[node.key] = {
type,
contents,
nodeDeps: nodeDeps[node.key],
nodeToNodeSubscriptions: nodeSubscriptions[node.key]
? nodeSubscriptions[node.key]
: [],
};
});
// React lifecycle hook on re-render
useEffect(() => {
// Window listener for messages from dev tool UI & background.js
window.addEventListener('message', onMessageReceived);
if (!isRestoredState) {
const devToolData = createDevToolDataObject(filteredSnapshot);
// Post message to content script on every re-render of the developers application only if content script has started
sendWindowMessage('recordSnapshot', devToolData);
} else {
isRestoredState = false;
}
// Clears the window event listener.
return () => window.removeEventListener('message', onMessageReceived);
});
// Listener callback for messages sent to windowf
const onMessageReceived = msg => {
// Add other actions from dev tool here
switch (msg.data.action) {
// Checks to see if content script has started before sending initial snapshot
case 'contentScriptStarted':
if (isPersistedState === 'false' || isPersistedState === null) {
const initialFilteredSnapshot = formatAtomSelectorRelationship(
filteredSnapshot,
);
// once application renders, grab the array of atoms and array of selectors
const appsKnownAtomsArray = [...snapshot._store.getState().knownAtoms]
// console.log('Store State.getState: Atoms', appsKnownAtomsArray);
const appsKnownSelectorsArray = [...snapshot._store.getState().knownSelectors]
// console.log('Store State.getState: Selectors', appsKnownSelectorsArray);
const atomsAndSelectorsMsg = {
atoms: appsKnownAtomsArray,
selectors: appsKnownSelectorsArray,
$selectors: selectorsObject // the selectors object that contain key and set / get methods as strings
}
//creating a indexDiff variable
//only created on initial creation of devToolData
//determines difference in length of backend snapshots array and frontend snapshotHistoryLength to avoid off by one error
const indexDiff = snapshots.length - 1;
const devToolData = createDevToolDataObject(
initialFilteredSnapshot,
indexDiff,
atomsAndSelectorsMsg,
);
sendWindowMessage('moduleInitialized', devToolData);
} else {
setProperIndexForPersistedState();
sendWindowMessage('persistSnapshots', null);
}
break;
// Listens for a request from dev tool to time travel to previous state of the app.
case 'snapshotTimeTravel':
timeTravelToSnapshot(msg);
break;
case 'persistState':
switchPersistMode();
break;
// Implementing the throttle change
case 'throttleEdit':
throttleLimit = parseInt(msg.data.payload.value);
break;
default:
break;
}
};
// assigns or switches isPersistedState in sessionStorage
const switchPersistMode = () => {
if (isPersistedState === 'false' || isPersistedState === null) {
// switch isPersistedState in sessionStorage to true
sessionStorage.setItem('isPersistedState', true);
// stores the length of current list of snapshots in sessionStorage
sessionStorage.setItem('persistedSnapshots', snapshots.length);
} else {
// switch isPersistedState in sessionStorage to false
sessionStorage.setItem('isPersistedState', false);
}
};
// function retreives length and fills snapshot array
const setProperIndexForPersistedState = () => {
const retreived = sessionStorage.getItem('persistedSnapshots');
const snapshotsArray = new Array(Number(retreived) + 1).fill({});
setSnapshots(snapshotsArray);
};
// Sends window an action and payload message.
const sendWindowMessage = (action, payload) => {
window.postMessage(
JSON.parse(JSON.stringify({
action,
payload,
})),
'*',
);
};
const createDevToolDataObject = (filteredSnapshot, diff, atomsAndSelectors) => {
if (diff === undefined) {
return {
filteredSnapshot: filteredSnapshot,
componentAtomTree: formatFiberNodes(
recoilizeRoot._reactRootContainer._internalRoot.current,
),
atomsAndSelectors,
};
} else {
return {
filteredSnapshot: filteredSnapshot,
componentAtomTree: formatFiberNodes(
recoilizeRoot._reactRootContainer._internalRoot.current,
),
indexDiff: diff,
atomsAndSelectors,
};
}
};
const formatAtomSelectorRelationship = filteredSnapshot => {
if (
window.$recoilDebugStates &&
Array.isArray(window.$recoilDebugStates) &&
window.$recoilDebugStates.length
) {
let snapObj =
window.$recoilDebugStates[window.$recoilDebugStates.length - 1];
if (snapObj.hasOwnProperty('nodeDeps')) {
for (let [key, value] of snapObj.nodeDeps) {
filteredSnapshot[key].nodeDeps = Array.from(value);
}
}
if (snapObj.hasOwnProperty('nodeToNodeSubscriptions')) {
for (let [key, value] of snapObj.nodeToNodeSubscriptions) {
filteredSnapshot[key].nodeToNodeSubscriptions = Array.from(value);
}
}
}
return filteredSnapshot;
};
// Will add hover effect over highlighted component
// Takes an argument of msg.data which contains name and payload
const activateHover = payload => {
let name = payload.name;
};
// FOR TIME TRAVEL: time travels to a given snapshot, re renders application.
const timeTravelToSnapshot = async msg => {
isRestoredState = true;
await gotoSnapshot(snapshots[msg.data.payload.snapshotIndex]);
};
// FOR TIME TRAVEL: Recoil hook to fire a callback on every atom/selector change -- research Throttle
useRecoilTransactionObserver_UNSTABLE(({snapshot}) => {
const now = new Date().getTime();
if (now - throttleTimer < throttleLimit) {
isRestoredState = true;
} else {
throttleTimer = now;
}
if (!isRestoredState) {
setSnapshots([...snapshots, snapshot]);
}
});
return null;
}
// function that receives objects to be passed into selector constructor function to post a message to the window
// cannot send an object with a property that contains a function to the window - need to stringify the set and get methods
export function formatRecoilizeSelectors(...selectors){
// create object to be sent via window message from target recoil application
selectorsObject = {};
// iterate through our array of objects
selectors.forEach(selector => {
// check if the current selector object contains a set method, if so, reassign it to a stringified version
if (selector.hasOwnProperty('set')){
selector.set = selector.set.toString();
}
// check if the current selector object contains a get method, if so, reassign it to a stringified version
if (selector.hasOwnProperty('get')){
selector.get = selector.get.toString();
}
// store the selector in the payload object - providing its property name as the 'key' property of the current selector object
// providing the object the property name of selector key will give easy searchability in GUI application for selector dropdown
selectorsObject[selector.key] = selector;
});
}
================================================
FILE: package/index.ts
================================================
import {useState, useEffect} from 'react';
import {
useRecoilTransactionObserver_UNSTABLE,
useRecoilSnapshot,
useGotoRecoilSnapshot,
} from 'recoil';
import formatFiberNodes from './formatFiberNodes';
// isRestored state disables snapshots from being recorded
let isRestoredState = false;
export default function RecoilizeDebugger(props: any) {
// We should ask for Array of atoms and selectors.
// Captures all atoms that were defined to get the initial state
let nodes = null;
if (typeof props.nodes === 'object' && !Array.isArray(props.nodes)) {
nodes = Object.values(props.nodes);
} else if (Array.isArray(props.nodes)) {
nodes = props.nodes;
}
const {root} = props;
const snapshot: any = useRecoilSnapshot();
// Local state of all previous snapshots to use for time traveling when requested by dev tools.
const [snapshots, setSnapshots] = useState([snapshot]);
// const [isRestoredState, setRestoredState] = useState(false);
const gotoSnapshot = useGotoRecoilSnapshot();
const filteredSnapshot: any = {};
const currentTree = snapshot._store.getState().currentTree;
// Traverse all atoms and selector state nodes and get value
nodes.forEach((node: any) => {
const type = node.__proto__.constructor.name;
const contents = snapshot.getLoadable(node).contents;
const nodeDeps = currentTree.nodeDeps.get(node.key);
const nodeToNodeSubscriptions = currentTree.nodeToNodeSubscriptions.get(
node.key,
);
// Construct node data structure for dev tool to consume
filteredSnapshot[node.key] = {
type,
contents,
nodeDeps: nodeDeps ? Array.from(nodeDeps) : [],
nodeToNodeSubscriptions: nodeToNodeSubscriptions
? Array.from(nodeToNodeSubscriptions)
: [],
};
});
// React lifecycle hook on re-render
useEffect(() => {
if (!isRestoredState) {
setTimeout(() => {
const devToolData = createDevToolDataObject(filteredSnapshot);
// Post message to content script on every re-render of the developers application only if content script has started
sendWindowMessage('recordSnapshot', devToolData);
}, 0);
} else {
isRestoredState = false;
}
// Window listener for messages from dev tool UI & background.js
window.addEventListener('message', onMessageReceived);
// Clears the window event listener.
return () => window.removeEventListener('message', onMessageReceived);
});
// Listener callback for messages sent to window
const onMessageReceived = (msg: any) => {
// Add other actions from dev tool here
switch (msg.data.action) {
// Checks to see if content script has started before sending initial snapshot
case 'contentScriptStarted':
const initialFilteredSnapshot = formatAtomSelectorRelationship(
filteredSnapshot,
);
const devToolData = createDevToolDataObject(initialFilteredSnapshot);
sendWindowMessage('moduleInitialized', devToolData);
break;
// Listens for a request from dev tool to time travel to previous state of the app.
case 'snapshotTimeTravel':
timeTravelToSnapshot(msg);
break;
default:
break;
}
};
// Sends window an action and payload message.
const sendWindowMessage = (action: any, payload: any) => {
window.postMessage(
{
action,
payload,
},
'*',
);
};
const createDevToolDataObject = (filteredSnapshot: any) => {
return {
filteredSnapshot: filteredSnapshot,
componentAtomTree: formatFiberNodes(
root._reactRootContainer._internalRoot.current,
),
};
};
const formatAtomSelectorRelationship = (filteredSnapshot: any) => {
const windowAny: any = window;
if (
windowAny.$recoilDebugStates &&
Array.isArray(windowAny.$recoilDebugStates) &&
windowAny.$recoilDebugStates.length
) {
let snapObj =
windowAny.$recoilDebugStates[windowAny.$recoilDebugStates.length - 1];
if (snapObj.hasOwnProperty('nodeDeps')) {
for (let [key, value] of snapObj.nodeDeps) {
filteredSnapshot[key].nodeDeps = Array.from(value);
}
}
if (snapObj.hasOwnProperty('nodeToNodeSubscriptions')) {
for (let [key, value] of snapObj.nodeToNodeSubscriptions) {
filteredSnapshot[key].nodeToNodeSubscriptions = Array.from(value);
}
}
}
return filteredSnapshot;
};
// FOR TIME TRAVEL: time travels to a given snapshot, re renders application.
const timeTravelToSnapshot = async (msg: any) => {
isRestoredState = true;
await gotoSnapshot(snapshots[msg.data.payload.snapshotIndex]);
};
// FOR TIME TRAVEL: Recoil hook to fire a callback on every snapshot change
useRecoilTransactionObserver_UNSTABLE(({snapshot}) => {
if (!isRestoredState) {
setSnapshots([...snapshots, snapshot]);
}
});
return null;
}
================================================
FILE: package/package.json
================================================
{
"name": "recoilize",
"version": "1.0.0",
"description": "Recoil Dev Tool",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/open-source-labs/Recoilize"
},
"peerDependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"recoil": "^0.1.2"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Bren Yamaguchi, Saejin Kang, Jonathan Escamilla, Sean Smith, Justin Choo, Anthony Lin, Spenser Schwartz, Steven Nguyen, Henry Taing, Seungho Baek, Taven Shumaker, Aaron Yang, Jesus Vargas, Davide Molino, Janis Hernandez, Jaime Baik, Anthony Magallanes, Edward Shei, Leonard Lew, Joey Ma, Harvey Nguyen, Victor Wang",
"license": "MIT",
"devDependencies": {
"typescript": "^3.9.6"
}
}
================================================
FILE: package.json
================================================
{
"name": "recoilize",
"jest": {
"setupFiles": [
"jest-webextension-mock"
]
},
"version": "3.0.0",
"description": "A Chrome extension that helps debug Recoil applications by memorizing the state of components with every render.",
"main": "index.js",
"scripts": {
"build": "webpack --mode production",
"dev": "webpack --mode development --watch",
"test": "jest --verbose",
"lint": "eslint '**/*.tsx' '**/*.ts' '**/*.js' --ignore-path .gitignore"
},
"author": "Bren Yamaguchi, Saejin Kang, Jonathan Escamilla, Sean Smith, Justin Choo, Anthony Lin, Spenser Schwartz, Steven Nguyen, Henry Taing, Seungho Baek, Taven Shumaker, Aaron Yang, Jesus Vargas, Davide Molino, Janis Hernandez, Jaime Baik, Anthony Magallanes, Edward Shei, Leonard Lew, Joey Ma, Harvey Nguyen, Victor Wang",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.10.3",
"@babel/polyfill": "^7.10.4",
"@babel/preset-env": "^7.10.3",
"@babel/preset-react": "^7.10.1",
"@babel/preset-typescript": "^7.10.4",
"@testing-library/jest-dom": "^5.11.0",
"@testing-library/react": "^10.4.6",
"@types/react": "^16.9.41",
"@types/react-dom": "^16.9.8",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"css-loader": "^3.6.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"eslint": "^7.4.0",
"eslint-config-fbjs": "^3.1.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.3",
"jest": "^26.1.0",
"jest-webextension-mock": "^3.6.1",
"prettier": "^2.0.5",
"recoil": "^0.7.2",
"style-loader": "^2.0.0",
"ts-loader": "^7.0.5",
"typescript": "^3.9.6",
"webpack": "^4.43.0",
"webpack-chrome-extension-reloader": "^1.3.0",
"webpack-cli": "^3.3.12"
},
"dependencies": {
"@angular/cli": "^10.0.5",
"@angular/core": "^10.0.8",
"@popperjs/core": "^2.4.4",
"@reduxjs/toolkit": "^1.5.0",
"@testing-library/react-hooks": "^3.4.1",
"@types/chrome": "0.0.117",
"@types/react-html-parser": "^2.0.1",
"@types/react-json-tree": "^0.6.11",
"@types/react-redux": "^7.1.16",
"@vx/group": "0.0.196",
"@vx/hierarchy": "0.0.196",
"@vx/responsive": "0.0.196",
"babel-eslint": "^10.1.0",
"codemirror": "^5.65.2",
"d3": "^5.16.0",
"d3-hierarchy": "1.1.9",
"d3-interpolate": "1.4.0",
"d3-scale": "3.2.1",
"d3-scale-chromatic": "1.5.0",
"d3-shape": "1.3.7",
"jest-webextension-mock": "^3.6.1",
"jsondiffpatch": "^0.4.1",
"multiselect-react-dropdown": "^1.5.7",
"prop-types": "^15.7.2",
"rc-slider": "^9.7.5",
"rc-tooltip": "^5.1.1",
"react": "^16.13.1",
"react-codemirror2": "^7.2.1",
"react-dom": "^16.13.1",
"react-html-parser": "^2.0.2",
"react-json-tree": "^0.11.2",
"react-multi-select-component": "^3.0.1",
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
"react-spring": "^8.0.27",
"react-testing-library": "^8.0.1",
"redux-persist": "^6.0.0",
"rxjs": "^6.6.2",
"semantic-ui-react": "^1.1.1",
"style-loader": "^2.0.0"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
}
}
================================================
FILE: src/README.md
================================================
# Developer README
## Brief
Though Recoil.js is still in the experimental state, it has proved its ability and catched a tremendous amount of attentions from experience developers for the past three years. We created Recoilize with one great mission in mind - providing a helpful tool so the developers can make an easy transition to Recoil.js and making the debugging process more effective. Recoilize is an open source product that is maintained and iterated constantly, and we're always welcome developers who are interested in it. Getting on board and understanding the codebase are never easy, so here are some useful tips and information that will help you get started quickly.
## File Structure
The scr folder contains Recoilize's source code - frontend and chrome extension.
```
src/
├── app/ # Frontend code
│ ├── components/ # React components
│ ├── Containers/ # More React components
│ ├── state-management/ # Redux Toolkit Slices
│ ├── utils/ # Helper Functions
│ └── index.tsx # Starting point for root App component
│
├── types/
│ └── index.d.ts #
│
├── extension/ # Chrome Extension code
│ ├── build/ # Destination for bundles and manifest.json (Chrome config file)
│ │ #
│ ├── background.js # Chrome Background Script
│ └── contentScript.ts # Chrome Content Script
└──
```
Here is an in-depth view of the app's components:

## Diagramming
If there's an update in the file structure, we suggest using [excalidraw](https://excalidraw.com/)
## Future Features and Possible Improvements
- Optimizing Time Travel Algorithm. The Time Travel Algorithm is working perfectly. However, we believe that it can be better by implementing a better algorithm (or different approaches). Please note that there's no right or wrong approaches, and everyone is welcome to try new ideas.
- UI/UX improvement. The UI/UX aspect definitely has room for improvement. Please feel free to play around with the app to get some ideas. For example, look at the component graph, atom network, graphs (flame, ranked, comparison).
- Testing is an important aspect, and we believe in TDD (Test Driven Development). However, due to time constraint and the limitation of resources, Recoilize is missing the testing part. Since testing has so much potential to improve, we strongly recommend developers who love testing get started as soon as you get onboard.
- Documentations - Documentations often get neglected since developers usually focus on writing code to improve features. However, at Recoilize, we believe that documenting is one of the most important tasks. Why? Because it's helpful not only for you, your teammates but it can also be valuable for future developers who interested in Recoilize. So if you love Recoilize, take good care of it by writing good documentations.
- Containerization - Have you ever wondered why the app that you made ran perfectly on your computer but it couldn't run on other's computers. That is a common problem that software engineers often encounter. We can solve this by containerize our app, and `Docker` is a good candidate for this task.
================================================
FILE: src/app/Containers/ButtonsContainer.tsx
================================================
import React from 'react';
const ButtonsContainer = () => {
const howToUseHandler = () => {
window.open('https://github.com/open-source-labs/Recoilize', '_blank');
};
return (
);
};
export default ButtonsContainer;
================================================
FILE: src/app/Containers/MainContainer.tsx
================================================
import React from 'react';
import SnapshotsContainer from './SnapshotContainer';
import VisualContainer from './VisualContainer';
import TravelContainer from './TravelContainer';
const MainContainer: React.FC = () => {
return (
);
};
export default MainContainer;
================================================
FILE: src/app/Containers/SnapshotContainer.tsx
================================================
import React, {useEffect, useState, useRef} from 'react';
import {selectFilterState} from '../state-management/slices/FilterSlice';
import {useAppSelector, useAppDispatch} from '../state-management/hooks';
import {setRenderIndex} from '../state-management/slices/SnapshotSlice';
const SnapshotsContainer: React.FC = () => {
const dispatch = useAppDispatch();
const snapshotHistory = useAppSelector(
state => state.snapshot.snapshotHistory,
);
const selected = useAppSelector(state => state.selected.selectedData);
const renderIndex = useAppSelector(state => state.snapshot.renderIndex);
const filterData = useAppSelector(selectFilterState);
const snapshotEndRef = useRef(null);
let snapshotHistoryLength = snapshotHistory.length;
// useEffect to scroll bottom whenever snapshot history changes
useEffect(() => {
scrollToBottom();
}, [snapshotHistoryLength]);
// using scrollInToView makes a smoother scroll
const scrollToBottom = (): void => {
snapshotEndRef.current.scrollIntoView({behavior: 'smooth'});
};
const snapshotDivs: JSX.Element[] = [];
// iterate the same length of our snapshotHistory
for (let i = 0; i < snapshotHistoryLength; i++) {
// filterFunc will return false if there is no change to state
const filterFunc = (): boolean => {
// don't use the counter for this, not reliable
if (i === 0) {
return true;
}
// checks if the filteredSnapshot object at index i has a key (atom or selector) found in the selected array. This would indicate that there was a change to that state/selector because filter is an array of objects containing differences between snapshots.
if (filterData[i]) {
for (let key in filterData[i].filteredSnapshot) {
for (let j = 0; j < selected.length; j++) {
if (key === selected[j].name) {
return true;
}
}
}
}
return false;
};
const x: boolean = filterFunc();
if (x === false) {
continue;
}
// renderTime is set equal to the actualDuration. If i is zero then we are obtaining actualDuration from the very first snapshot in snapshotHistory. This is to avoid having undefined filter elements since there will be no difference between snapshot at the first instance.
let renderTime: number =
snapshotHistory[i].componentAtomTree.treeBaseDuration;
// if (i === 0) {
// renderTime = snapshotHistory[0].componentAtomTree.treeBaseDuration;
// }
// //Checks to see if the actualDuration within filter is an array. If it is an array then the 2nd value in the array is the new actualDuration.
// else if (Array.isArray(filterData[i].componentAtomTree.actualDuration)) {
// renderTime = filterData[i].componentAtomTree.treeBaseDuration[1];
// } else {
// renderTime = filterData[i].componentAtomTree.treeBaseDuration;
// }
// Push a div container to snapshotDivs array only if there was a change to state.
// The div container will contain renderTimes evaluated above.
snapshotDivs.push(
{
dispatch(setRenderIndex(i));
}}>
{/*
{i}
*/}
{`${Math.round(renderTime * 100) / 100}ms`}
,
);
}
//indexDiff is used to ensure the index of filter matches the index of the snapshots array in the backend
let indexDiff: number = 0;
if (filterData[0] && filterData[0].indexDiff) {
indexDiff = filterData[0].indexDiff;
}
// functionality to postMessage the selected snapshot index to background.js
const timeTravelFunc = (index: number) => {
// variable to store/reference connection
const backgroundConnection = chrome.runtime.connect();
//const test = chrome.extension.getBackgroundPage();
// post the message with index in payload to the connection
backgroundConnection.postMessage({
action: 'snapshotTimeTravel',
tabId: chrome.devtools.inspectedWindow.tabId,
payload: {
snapshotIndex: index + indexDiff,
},
});
};
function prevClr() {
const snapshotListArr = document.querySelectorAll('.individualSnapshot');
for (let i = 0; i < snapshotListArr.length; i++) {
let index = parseInt(snapshotListArr[i].id.match(/\d+/g)[0]);
if (index < renderIndex) {
snapshotListArr[i].parentNode.removeChild(snapshotListArr[i]);
} else break;
}
}
function fwrdClr() {
const snapshotListArr = document.querySelectorAll('.individualSnapshot');
for (let i = snapshotListArr.length - 1; i >= 0; i--) {
let index = parseInt(snapshotListArr[i].id.match(/\d+/g)[0]);
if (index > renderIndex) {
snapshotListArr[i].parentNode.removeChild(snapshotListArr[i]);
} else break;
}
}
// create a function to store current data to local storage
const toLocalStorage = (data: any) => {
for (let i = 0; i < data.length; i++) {
console.log('trigger toLocalStorage');
const jsonData = JSON.stringify(data[i]);
localStorage.setItem(`${i}`, jsonData);
}
};
return (
Clear Snapshots
Snapshots
{snapshotDivs}
);
};
export default SnapshotsContainer;
================================================
FILE: src/app/Containers/TravelContainer.tsx
================================================
import React from 'react';
import MainSlider from '../components/Slider/MainSlider';
import ButtonsContainer from './ButtonsContainer';
const TravelContainer: React.FC = () => {
return (
);
};
export default TravelContainer;
================================================
FILE: src/app/Containers/VisualContainer.tsx
================================================
import React, {useState} from 'react';
import {RecoilRoot} from 'recoil';
import Diff from '../components/StateDiff/Diff';
import NavBar from '../components/NavBar/NavBar';
import Metrics from '../components/Metrics/MetricsContainer';
import Tree from '../components/StateTree/Tree';
import Network from '../components/AtomNetwork/AtomNetwork';
import AtomComponentVisualContainer from '../components/ComponentGraph/AtomComponentContainer';
import Settings from '../components/Settings/SettingsContainer';
import Testing from '../components/Testing/TestingContainer';
type navTypes = {
[tabName: string]: JSX.Element;
};
// Renders Navbar and conditionally renders Diff, Visualizer, and Tree
const VisualContainer: React.FC = () => {
// object containing all conditional renders based on navBar
const nav: navTypes = {
// compare the diff of filteredPrevSnap and filteredCurSnap
'State Diff': ,
// render JSON tree of snapshot
'State Tree': ,
// tree visualizer of components showing atom/selector relationships
'Component Graph': ,
// atom and selector subscription relationship
'Atom Network': ,
// quotes not needed where name = component variable
// individual snapshot visualizer
Metrics: ,
// settings tab
Settings: ,
// add a testing tab
Testing: (
),
};
// array of all nav obj keys
const tabsList: string[] = Object.keys(nav);
// useState hook to update which component to render in the VisualContainer
const [tab, setTab] = useState('State Diff');
// conditionally render based on value of nav[tab]
return (
{nav[tab]}
);
};
export default VisualContainer;
================================================
FILE: src/app/Containers/__tests__/MainContainer.unit.test.js
================================================
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import {getQueriesForElement, getByText} from '@testing-library/dom';
import {render, fireEvent} from '@testing-library/react';
import MainContainer from '../MainContainer';
it('Main Container Renders', () => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const {getByPlaceholderText, debug} = render(
,
);
});
================================================
FILE: src/app/Containers/__tests__/SnapshotContainer.unit.test.js
================================================
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import {getQueriesForElement, getByText} from '@testing-library/dom';
import {render, fireEvent} from '@testing-library/react';
import SnapshotContainer from '../SnapshotContainer';
it('Snapshot Container Renders', () => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const {getByPlaceholderText, debug} = render(
,
);
});
================================================
FILE: src/app/Containers/__tests__/VisualContainer.unit.test.js
================================================
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import {getQueriesForElement, getByText} from '@testing-library/dom';
import {render, fireEvent} from '@testing-library/react';
import VisualContainer from '../VisualContainer';
it('Visual Container Renders', () => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const {getByPlaceholderText, debug} = render(
,
);
});
================================================
FILE: src/app/components/App.tsx
================================================
import React, {useEffect} from 'react';
import MainContainer from '../Containers/MainContainer';
import {selectedTypes} from '../../types';
// importing the diff to find difference
import {diff} from 'jsondiffpatch';
import {useAppSelector, useAppDispatch} from '../state-management/hooks';
import {
setSnapshotHistory,
setRenderIndex,
setCleanComponentAtomTree,
} from '../state-management/slices/SnapshotSlice';
import {
addSelected,
setSelected,
} from '../state-management/slices/SelectedSlice';
import {
updateFilter,
selectFilterState,
} from '../state-management/slices/FilterSlice';
import {setAtomsAndSelectors} from '../state-management/slices/AtomsAndSelectorsSlice';
const LOGO_URL = './assets/Recoilize-v2.png';
const App: React.FC = () => {
const dispatch = useAppDispatch();
// useState hook to update the snapshotHistory array
// array of snapshots
const snapshotHistory = useAppSelector(
state => state.snapshot.snapshotHistory,
);
const renderIndex = useAppSelector(state => state.snapshot.renderIndex);
const selected = useAppSelector(state => state.selected.selectedData);
// selected will be an array with objects containing filteredSnapshot key names (the atoms and selectors)
// ex: [{name: 'Atom1'}, {name: 'Atom2'}, {name: 'Selector1'}, ...]
// const [selected, setSelected] = useState([]);
// todo: Create algo that will clean up the big setSnapshothistory object, now and before
// ! Setting up the selected
const filterData = useAppSelector(selectFilterState);
// Whenever snapshotHistory changes, useEffect will run, and selected will be updated
useEffect(() => {
// whenever snapshotHistory changes, update renderIndex
dispatch(setRenderIndex(snapshotHistory.length - 1));
let last;
if (snapshotHistory[renderIndex]) {
last = snapshotHistory[renderIndex].filteredSnapshot;
}
// we must compare with the original
for (let key in last) {
if (!snapshotHistory[0].filteredSnapshot[key]) {
// only push if the name doesn't already exist
const check = () => {
for (let i = 0; i < selected.length; i++) {
// break if it exists
if (selected[i].name === key) {
return true;
}
}
// does not exist
return false;
};
if (!check()) {
// console.log('after Check');
dispatch(addSelected({name: key}));
}
}
}
}, [snapshotHistory]); // Only re-run the effect if snapshot history changes -- react hooks
//Update cleanComponentAtomTree as Render Index changes
useEffect(() => {
if (snapshotHistory.length === 0) return;
dispatch(
setCleanComponentAtomTree(snapshotHistory[renderIndex].componentAtomTree),
);
}, [renderIndex]);
// useEffect for snapshotHistory
useEffect(() => {
// SETUP connection to bg script
const backgroundConnection = chrome.runtime.connect();
// INITIALIZE connection to bg script
backgroundConnection.postMessage({
action: 'devToolInitialized',
tabId: chrome.devtools.inspectedWindow.tabId,
});
// console.log(
// 'here is the background connection post message IN APP',
// backgroundConnection,
// );
// LISTEN for messages FROM bg script
backgroundConnection.onMessage.addListener(msg => {
if (msg.action === 'recordSnapshot') {
// ! sets the initial selected
//console.log('should have our atoms and selectors: ', msg.payload);
if (!msg.payload[1]) {
// ensures we only set initially
const arr: selectedTypes[] = [];
for (let key in msg.payload[0].filteredSnapshot) {
arr.push({name: key});
}
// setSelected(arr);
//console.log('arr in App.tsx send to setSelected', arr)
dispatch(setSelected(arr));
}
// console.log(
// 'this is snapshotHistory',
// msg.payload[msg.payload.length - 1],
// );
dispatch(setSnapshotHistory(msg.payload[msg.payload.length - 1]));
// update state with the atoms and selectors!!!
dispatch(setAtomsAndSelectors(msg.payload[0].atomsAndSelectors));
//console.log('Payload: IS IT HERE??, ', msg.payload);
// ! Setting the FILTER Array
if (!msg.payload[1] && filterData.length === 0) {
// todo: currently the filter does not work if recoilize is not open, we must change msg.payload to incorporate delta function in the backend
dispatch(updateFilter(msg.payload));
} else {
// push the difference between the objects
const delta = diff(
msg.payload[msg.payload.length - 2],
msg.payload[msg.payload.length - 1],
);
// only push if the snapshot length is chill
if (filterData.length < msg.payload.length) {
dispatch(updateFilter([delta]));
}
}
}
});
}, []);
// Render main container if we have detected a recoil app with the recoilize module passing data
const renderMainContainer: JSX.Element = ;
// Render module not found message if snapHistory is null, this means we have not detected a recoil app with recoilize module installed properly
const renderModuleNotFoundContainer: JSX.Element = (