Skip to content

Commit f597535

Browse files
committed
Modularize state & prop data processing
1 parent 520e55b commit f597535

File tree

3 files changed

+341
-320
lines changed

3 files changed

+341
-320
lines changed

src/backend/helpers.ts

Lines changed: 0 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010
import { string } from 'prop-types';
1111

1212
// eslint-disable-next-line import/newline-after-import
13-
const acorn = require('acorn');
14-
const jsx = require('acorn-jsx');
1513

16-
const JSXParser = acorn.Parser.extend(jsx());
1714

1815
/**
1916
* @method throttle
@@ -96,121 +93,3 @@ export const throttle = (
9693
};
9794
};
9895

99-
export const getHooksNames = (
100-
elementType: string,
101-
): { hookName: string; varName: string }[] | undefined => {
102-
// Initialize empty object to store the setters and getter
103-
let AST: any;
104-
try {
105-
AST = JSXParser.parse(elementType);
106-
} catch (e) {
107-
throw Error('Error occurs at helpers getHooksName.ts Cannot parse functional component.');
108-
}
109-
// Begin search for hook names, only if ast has a body property.
110-
111-
AST = AST.body;
112-
113-
// Statements get all the names of the hooks. For example: useCount, useWildcard, ...
114-
const statements: { hookName: string; varName: string }[] = [];
115-
/** All module exports always start off as a single 'FunctionDeclaration' type
116-
* Other types: "BlockStatement" / "ExpressionStatement" / "ReturnStatement"
117-
* Iterate through AST of every function declaration
118-
* Check within each function declaration if there are hook declarations & variable name declaration */
119-
AST.forEach((functionDec: any) => {
120-
let declarationBody: any;
121-
if (functionDec.expression?.body) declarationBody = functionDec.expression.body.body;
122-
// check if functionDec.expression.body exists, then set declarationBody to functionDec's body
123-
else declarationBody = functionDec.body?.body ?? [];
124-
// Traverse through the function's funcDecs and Expression Statements
125-
declarationBody.forEach((elem: any) => {
126-
// Hooks will always be contained in a variable declaration
127-
if (elem.type === 'VariableDeclaration') {
128-
// Obtain the declarations array from elem.
129-
const { declarations } = elem;
130-
// Obtain the reactHook:
131-
const reactHook: string = declarations[0]?.init?.callee?.expressions[1]?.property?.name;
132-
if (reactHook === 'useState') {
133-
// Obtain the variable being set:
134-
let varName: string = declarations[1]?.id?.name;
135-
// Obtain the setState method:
136-
let hookName: string = declarations[2]?.id?.name;
137-
// Push reactHook & varName to statements array
138-
statements.push({ hookName, varName });
139-
}
140-
}
141-
});
142-
});
143-
return statements;
144-
};
145-
146-
// DEPERACATED: After React DEV Tool Update. This function no longer works. Keep for history record
147-
// // Helper function to grab the getters/setters from `elementType`
148-
// /**
149-
// * @method getHooksNames
150-
// * @param elementType The fiber `type`, A stringified function of the component, the Fiber whose hooks we want corresponds to
151-
// * @returns An array of strings
152-
// */
153-
// // TODO: this function has return at the end of loop? Is this intentional?
154-
// export const getHooksNames = (elementType: string): Array<string> | undefined => {
155-
// // Initialize empty object to store the setters and getter
156-
// let ast: any;
157-
// try {
158-
// ast = JSXParser.parse(elementType);
159-
// } catch (e) {
160-
// return ['unknown'];
161-
// }
162-
// // hookNames will contain an object with methods (functions)
163-
// const hooksNames: any = {};
164-
165-
// // Begin search for hook names, only if ast has a body property.
166-
// while (Object.hasOwnProperty.call(ast, 'body')) {
167-
// let tsCount = 0; // Counter for the number of TypeScript hooks seen (to distinguish in masterState)
168-
// ast = ast.body;
169-
170-
// // Statements get all the names of the hooks. For example: useCount, useWildcard, ...
171-
// const statements: Array<string> = [];
172-
// /** All module exports always start off as a single 'FunctionDeclaration' type
173-
// * Other types: "BlockStatement" / "ExpressionStatement" / "ReturnStatement"
174-
// * Iterate through AST of every function declaration
175-
// * Check within each function declaration if there are hook declarations */
176-
// ast.forEach((functionDec: any) => {
177-
// let declarationBody: any;
178-
// if (functionDec.expression?.body) declarationBody = functionDec.expression.body.body;
179-
// // check if functionDec.expression.body exists, then set declarationBody to functionDec's body
180-
// else declarationBody = functionDec.body?.body ?? [];
181-
// // Traverse through the function's funcDecs and Expression Statements
182-
// declarationBody.forEach((elem: any) => {
183-
// // Hooks will always be contained in a variable declaration
184-
// if (elem.type === 'VariableDeclaration') {
185-
// elem.declarations.forEach((hook: any) => {
186-
// // Parse destructured statements pair
187-
// if (hook.id.type === 'ArrayPattern') {
188-
// hook.id.elements.forEach((hook: any) => {
189-
// statements.push(`_useWildcard${tsCount}`);
190-
// statements.push(hook.name);
191-
// tsCount += 1;
192-
// });
193-
// // Process hook function invocation ?
194-
// } else {
195-
// // hook.init.object is '_useState2', '_useState4', etc.
196-
// // eslint-disable-next-line no-lonely-if
197-
// if (hook?.init?.object?.name) {
198-
// const varName: any = hook.init.object.name;
199-
// if (!hooksNames[varName] && varName.match(/_use/)) {
200-
// hooksNames[varName] = hook.id.name;
201-
// }
202-
// }
203-
// if (hook.id.name !== undefined) {
204-
// statements.push(hook.id.name);
205-
// }
206-
// }
207-
// });
208-
// }
209-
// });
210-
// statements.forEach((el, i) => {
211-
// if (el.match(/_use/)) hooksNames[el] = statements[i + 1];
212-
// });
213-
// });
214-
// return Object.values(hooksNames);
215-
// }
216-
// };

src/backend/linkFiber.ts

Lines changed: 13 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ import updateSnapShotTree from './snapShot';
6363

6464
// throttle returns a function that can be called any number of times (possibly in quick succession) but will only invoke the callback at most once every x ms
6565
// getHooksNames - helper function to grab the getters/setters from `elementType`
66-
import { throttle, getHooksNames } from './helpers';
66+
import { throttle } from './helpers';
67+
import {
68+
getHooksNames,
69+
getHooksStateAndUpdateMethod,
70+
getStateAndContextData,
71+
filterAndFormatData,
72+
} from './statePropsExtractors';
6773

6874
// Set global variables to use in exported module and helper functions
6975
declare global {
@@ -73,198 +79,6 @@ declare global {
7379
}
7480
}
7581

76-
// TODO: Determine what Component Data Type we are sending back for state, context, & props
77-
type ReactimeData = {
78-
[key: string]: any;
79-
};
80-
81-
/**
82-
* @function traverseHooks
83-
* @param memoizedState - The current state of the component associated with the current Fiber node.
84-
* @return An array of array of HookStateItem objects
85-
*
86-
* Helper function to traverse through memoizedState and inject instrumentation to update our state tree
87-
* every time a hooks component changes state
88-
*/
89-
function traverseHooks(memoizedState: any): HookStates {
90-
const hooksStates: HookStates = [];
91-
while (memoizedState) {
92-
if (memoizedState.queue) {
93-
hooksStates.push({
94-
component: memoizedState.queue,
95-
state: memoizedState.memoizedState,
96-
});
97-
}
98-
memoizedState = memoizedState.next;
99-
}
100-
return hooksStates;
101-
}
102-
103-
const exclude = new Set([
104-
'alternate',
105-
'basename',
106-
'baseQueue',
107-
'baseState',
108-
'child',
109-
'childLanes',
110-
'children',
111-
'Consumer',
112-
'context',
113-
'create',
114-
'deps',
115-
'dependencies',
116-
'destroy',
117-
'dispatch',
118-
'location',
119-
'effects',
120-
'element',
121-
'elementType',
122-
'firstBaseUpdate',
123-
'firstEffect',
124-
'flags',
125-
'get key',
126-
'getState',
127-
'hash',
128-
'key',
129-
'lanes',
130-
'lastBaseUpdate',
131-
'lastEffect',
132-
'liftedStore',
133-
'navigator',
134-
'memoizedState',
135-
'mode',
136-
'navigationType',
137-
'next',
138-
'nextEffect',
139-
'pending',
140-
'parentSub',
141-
'pathnameBase',
142-
'pendingProps',
143-
'Provider',
144-
'updateQueue',
145-
'ref',
146-
'replaceReducer',
147-
'responders',
148-
'return',
149-
'route',
150-
'routeContext',
151-
'search',
152-
'shared',
153-
'sibling',
154-
'state',
155-
'store',
156-
'subscribe',
157-
'subscription',
158-
'stateNode',
159-
'tag',
160-
'type',
161-
'_calculateChangedBits',
162-
'_context',
163-
'_currentRenderer',
164-
'_currentRenderer2',
165-
'_currentValue',
166-
'_currentValue2',
167-
'_owner',
168-
'_self',
169-
'_source',
170-
'_store',
171-
'_threadCount',
172-
'$$typeof',
173-
'@@observable',
174-
]);
175-
// ------------FILTER DATA FROM REACT DEV TOOL && CONVERT TO STRING-------------
176-
/**
177-
* This function receives raw Data from REACT DEV TOOL and filter the Data based on the exclude list. The filterd data is then converted to string (if applicable) before being send to reacTime front end.
178-
*
179-
* @param reactDevData - The data object obtained from React Devtool. Ex: memoizedProps, memoizedState
180-
* @param reactimeData - The cached data from the current component. This can be data about states, context and/or props of the component.
181-
* @returns The update component data object to send to front end of ReactTime
182-
*/
183-
function convertDataToString(
184-
reactDevData: { [key: string]: any },
185-
reactimeData: ReactimeData = {},
186-
): ReactimeData {
187-
for (const key in reactDevData) {
188-
try {
189-
// Skip keys that are in exclude set OR if there is no value at key
190-
// Falsy values such as 0, false, null are still valid value
191-
if (exclude.has(key) || reactDevData[key] === undefined) {
192-
continue;
193-
}
194-
// If value at key is a function, assign key with value 'function' to reactimeData object
195-
else if (typeof reactDevData[key] === 'function') {
196-
reactimeData[key] = 'function';
197-
}
198-
// If value at key is an object/array and not null => JSON stringify the value
199-
else if (typeof reactDevData[key] === 'object' && reactDevData[key] !== null) {
200-
reactimeData[key] = JSON.stringify(reactDevData[key]);
201-
}
202-
// Else assign the primitive value
203-
else {
204-
reactimeData[key] = reactDevData[key];
205-
}
206-
} catch (err) {
207-
console.log('linkFiber', { reactDevData, key });
208-
throw Error(`Error caught at converDataToString: ${err}`);
209-
}
210-
}
211-
return reactimeData;
212-
}
213-
// ------------------------FILTER STATE & CONTEXT DATA--------------------------
214-
/**
215-
* This function is used to trim the state data of a component.
216-
* All propperty name that are in the central exclude list would be trimmed off.
217-
* If passed in memoizedState is a not an object (a.k.a a primitive type), a default name would be provided.
218-
* @param memoizedState - The current state of the component associated with the current Fiber node.
219-
* @param _debugHookTypes - An array of hooks used for debugging purposes.
220-
* @param componentName - Name of the evaluated component
221-
* @returns - The updated state data object to send to front end of ReactTime
222-
*/
223-
function trimContextData(
224-
memoizedState: Fiber['memoizedState'],
225-
componentName: string,
226-
_debugHookTypes: Fiber['_debugHookTypes'],
227-
) {
228-
// Initialize a list of componentName that would not be evaluated for State Data:
229-
const ignoreComponent = new Set(['BrowserRouter', 'Router']);
230-
if (ignoreComponent.has(componentName)) return;
231-
232-
// Initialize object to store state and context data of the component
233-
const reactimeData: ReactimeData = {};
234-
// Initialize counter for the default naming. If user use reactHook, such as useState, react will only pass in the value, and not the variable name of the state.
235-
let stateCounter = 1;
236-
let refCounter = 1;
237-
238-
// Loop through each hook inside the _debugHookTypes array.
239-
// NOTE: _debugHookTypes.length === height of memoizedState tree.
240-
for (const hook of _debugHookTypes) {
241-
// useContext does not create any state => skip
242-
if (hook === 'useContext') {
243-
continue;
244-
}
245-
// If user use useState reactHook => React will only pass in the value of state & not the name of the state => create a default name:
246-
else if (hook === 'useState') {
247-
const defaultName = `State ${stateCounter}`;
248-
reactimeData[defaultName] = memoizedState.memoizedState;
249-
stateCounter++;
250-
}
251-
// If user use useRef reactHook => React will store memoizedState in current object:
252-
else if (hook === 'useRef') {
253-
const defaultName = `Ref ${refCounter}`;
254-
reactimeData[defaultName] = memoizedState.memoizedState.current;
255-
refCounter++;
256-
}
257-
// If user use Redux to contain their context => the context object will be stored using useMemo Hook, as of for Rect Dev Tool v4.27.2
258-
// Note: Provider is not a reserved component name for redux. User may name their component as Provider, which will break this logic. However, it is a good assumption that if user have a custom provider component, it would have a more specific naming such as ThemeProvider.
259-
else if (hook === 'useMemo' && componentName === 'Provider') {
260-
convertDataToString(memoizedState.memoizedState[0], reactimeData);
261-
}
262-
//Move on to the next level of memoizedState tree.
263-
memoizedState = memoizedState?.next;
264-
}
265-
// Return the updated state data object to send to front end of ReactTime
266-
return reactimeData;
267-
}
26882
// -------------------------CREATE TREE TO SEND TO FRONT END--------------------
26983
/**
27084
* This is a recursive function that runs after every Fiber commit using the following logic:
@@ -341,7 +155,7 @@ export function createTree(
341155
) {
342156
// Access the memoizedProps of the parent component
343157
const propsData = memoizedProps.children[0]._owner.memoizedProps;
344-
const newPropData = convertDataToString(
158+
const newPropData = filterAndFormatData(
345159
propsData,
346160
tree.componentData.props ? tree.componentData.props : null,
347161
);
@@ -395,7 +209,7 @@ export function createTree(
395209
componentData.props = { pathname: memoizedProps?.match?.pathname };
396210
break;
397211
default:
398-
Object.assign(componentData.props, convertDataToString(memoizedProps));
212+
Object.assign(componentData.props, filterAndFormatData(memoizedProps));
399213
}
400214
}
401215

@@ -411,14 +225,14 @@ export function createTree(
411225
if (elementType.name === 'Provider') {
412226
Object.assign(
413227
componentData.context,
414-
trimContextData(memoizedState, elementType.name, _debugHookTypes),
228+
getStateAndContextData(memoizedState, elementType.name, _debugHookTypes),
415229
);
416230
}
417231
// Else if user use ReactHook to define state => all states will be stored in memoizedState => grab all states stored in the memoizedState
418232
// else {
419233
// Object.assign(
420234
// componentData.state,
421-
// trimContextData(memoizedState, elementType.name, _debugHookTypes),
235+
// getStateAndContextData(memoizedState, elementType.name, _debugHookTypes),
422236
// );
423237
// }
424238
}
@@ -430,7 +244,7 @@ export function createTree(
430244
if (stateData === null || typeof stateData !== 'object') {
431245
stateData = { CONTEXT: stateData };
432246
}
433-
componentData.context = convertDataToString(stateData);
247+
componentData.context = filterAndFormatData(stateData);
434248
}
435249

436250
// DEPRECATED: This code might have worked previously. However, with the update of React Dev Tool, context can no longer be pulled using this method.
@@ -469,7 +283,7 @@ export function createTree(
469283
// so we must traverse through the list and get the states.
470284
// We then store them along with the corresponding memoizedState.queue,
471285
// which includes the dispatch() function we use to change their state.
472-
const hooksStates = traverseHooks(memoizedState);
286+
const hooksStates = getHooksStateAndUpdateMethod(memoizedState);
473287
const hooksNames = getHooksNames(elementType.toString());
474288
// Intialize state & index:
475289
newState.hooksState = [];

0 commit comments

Comments
 (0)