diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c31d41d9f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: node_js -node_js: - - 12 -dist: trusty -cache: npm -git: - submodules: false -# branches: -# only: -# - implementtravisci -install: - - npm install -script: - - npm run test \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b5872bcd3..000000000 --- a/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM node:10.16.2 -WORKDIR /usr/src/app -COPY package*.json ./ -RUN npm i diff --git a/LICENSE b/LICENSE index d19ddaf80..498003dd9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 reactime +Copyright (c) 2025 reactime Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7534d0384..b12b4a9f0 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ You can view your application's file structure and click on a snapshot to view your app's state. State can be visualized in a Component Graph, JSON Tree, or Performance Graph. Snapshot history can be visualized in the History tab. -The Web Metrics tab provides some useful metrics for site performance. The accessibility tab +The Web Metrics tab provides some useful metrics for site performance. The accessibility tab visualizes an app's accessibility tree per state change. Snapshots can be compared with the previous snapshot, which can be viewed in Diff mode.
@@ -89,7 +89,7 @@ Download the recorded snapshots as a JSON file and upload them to access state t


-### πŸ”Ή Reconnect and Status +### πŸ”Ή and Status If Reactime loses its connection to the tab you're monitoring, simply click the "reconnect" button to resume your work. You'll notice a circle located to the right of the button, which will appear as either red (indicating disconnection) or green (signifying a successful reconnection).
diff --git a/demo-app-next/next-env.d.ts b/demo-app-next/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/demo-app-next/next-env.d.ts +++ b/demo-app-next/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/demo-app-next/tsconfig.json b/demo-app-next/tsconfig.json index 093985aaf..1203d7cb2 100644 --- a/demo-app-next/tsconfig.json +++ b/demo-app-next/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -12,8 +16,15 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/demo-app/package.json b/demo-app/package.json index 17ed638c0..4003e82fa 100644 --- a/demo-app/package.json +++ b/demo-app/package.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@babel/core": "^7.16.7", + "@babel/plugin-transform-runtime": "^7.25.9", "@babel/preset-env": "^7.16.7", "@babel/preset-react": "^7.16.7", "@types/express": "^4.17.13", @@ -17,6 +18,7 @@ "@types/react-dom": "^17.0.19", "babel-loader": "^8.2.3", "copy-webpack-plugin": "^10.2.0", + "core-js": "^3.39.0", "css-loader": "^6.5.1", "html-webpack-plugin": "^5.5.0", "node": "^16.0.0", diff --git a/demo-app/src/client/Components/Buttons.tsx b/demo-app/src/client/Components/Buttons.tsx index b27abfe24..60235aa92 100644 --- a/demo-app/src/client/Components/Buttons.tsx +++ b/demo-app/src/client/Components/Buttons.tsx @@ -1,22 +1,82 @@ -import React from 'react'; -import Increment from './Increment'; +import React, { Component, useState } from 'react'; -function Buttons(): JSX.Element { - const buttons = []; - for (let i = 0; i < 4; i++) { - buttons.push(); +type ButtonProps = { + id: string; + label: string; + color?: string; + initialCount?: number; +}; + +type IncrementClassState = { + count: number; +}; + +class IncrementClass extends Component { + state = { + count: this.props.initialCount || 0, + }; + + handleClick = (): void => { + this.setState((prevState: IncrementClassState) => ({ + count: prevState.count + 1, + })); + }; + + render(): JSX.Element { + return ( +
+ +
+ ); } +} + +const IncrementFunction = (props: ButtonProps): JSX.Element => { + const [count, setCount] = useState(props.initialCount || 0); + + const handleClick = (): void => { + setCount((prev) => prev + 1); + }; return ( -
-

Stateful Buttons

-

- These buttons are functional components that each manage their own state with the useState - hook. -

- {buttons} +
+
); +}; + +class Buttons extends Component { + render(): JSX.Element { + return ( +
+

Mixed State Counter

+

First two buttons use class components, last two use function components.

+ + + + +
+ ); + } } export default Buttons; diff --git a/demo-app/src/client/Components/ButtonsWithMoreHooks.jsx b/demo-app/src/client/Components/ButtonsWithMoreHooks.jsx deleted file mode 100644 index 5d9398b51..000000000 --- a/demo-app/src/client/Components/ButtonsWithMoreHooks.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { useState, createContext } from 'react'; -import IncrementWithMoreHooks from './IncrementWithMoreHooks'; - -/** - * This component as well as IncrementWithMoreHooks were made to show where data for different - * hooks show up in the react fiber tree. - */ - -/** - * This file is a JSX file, not a TSX file, on purpose. The code won't be converted to common JS - * before being bundled by webpack. There were some errors that weren't showing up for the other - * Increment.tsx file based on how webpack uglifies ES6 files. Maintaining this as a JSX file - * will help check for these types of issues. - */ - -/** - * How Reactime extracts useState data and what would have to be done - * to extract useContext and useReducer data: - * - * When extracting a functional component's useState data from the fiber tree in the backend of - * Reactime, said data is stored on said component's fiber tree node at its memoizedState property, - * which is a linked list with each node holding data for each useState invocation (some but - * not all other hooks also store nodes with data here). Each useState memoizedState node includes - * the variable (e.g. user) and its corresponding dispatch function (e.g. setUser). This dispatch - * function is required to use Reactime's timeJump feature. - * - * useContext data is stored on the property "dependencies", and only the data passed into the - * value attritibute of the 'context'.Provider element will be there. For tripContext.Provider, - * we pass down "trip" without "setTrip", so we won't have access to the 'trip' dispatch function - * in the "IncrementWithMoreHooks" component, meaning Reactime's timeJump won't work without - * finding the 'trip' dispatch function by coming into this component where useState was invoked and - * storing it in the appropriate place. This is easy enough for useState variables, but useContext - * is commonly used with useReducer which is an entirely different beast. - * - * I advise solving the puzzle of making useReducer work with the timeJump functionality before - * integrating other hooks. Look at time jumping in the Redux dev tools chrome extension for - * inspiration, because you essentially need to recreate that within Reactime. - */ - -// userInCreateContext is different from 'user' to see where this variable name showed up in the AST -export const userContext = createContext({ userInCreateContext: 'null', setUser: undefined }); -export const tripContext = createContext({ trip: 'null', setTrip: undefined }); - -const ButtonsWithMoreHooks = () => { - const [user, setUser] = useState('null'); - const userValue = { user, setUser }; - const [trip, setTrip] = useState('Hawaii'); - const tripValue = { trip }; - - return ( -
- - - - - -
- ); -}; - -export default ButtonsWithMoreHooks; diff --git a/demo-app/src/client/Components/FunctionalReducerCounter.tsx b/demo-app/src/client/Components/FunctionalReducerCounter.tsx new file mode 100644 index 000000000..85effd454 --- /dev/null +++ b/demo-app/src/client/Components/FunctionalReducerCounter.tsx @@ -0,0 +1,231 @@ +import React, { useState, useReducer } from 'react'; + +type CounterProps = { + initialCount?: number; + step?: number; + title?: string; + theme?: { + backgroundColor?: string; + textColor?: string; + }; +}; + +type CounterState = { + count: number; + history: number[]; + lastAction: string; +}; + +type CounterAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'DOUBLE' } + | { type: 'RESET' } + | { type: 'ADD'; payload: number } + | { type: 'SET_STATE'; payload: CounterState }; + +type SecondaryCounterState = { + count: number; + multiplier: number; + lastOperation: string; + history: number[]; +}; + +type SecondaryCounterAction = + | { type: 'MULTIPLY' } + | { type: 'DIVIDE' } + | { type: 'SET_MULTIPLIER'; payload: number } + | { type: 'RESET' } + | { type: 'SET_STATE'; payload: SecondaryCounterState }; + +function counterReducer(state: CounterState, action: CounterAction, step: number): CounterState { + switch (action.type) { + case 'INCREMENT': + return { + ...state, + count: state.count + step, + history: [...state.history, state.count + step], + lastAction: 'INCREMENT', + }; + case 'DECREMENT': + return { + ...state, + count: state.count - step, + history: [...state.history, state.count - step], + lastAction: 'DECREMENT', + }; + case 'DOUBLE': + return { + ...state, + count: state.count * 2, + history: [...state.history, state.count * 2], + lastAction: 'DOUBLE', + }; + case 'RESET': + return { + count: 0, + history: [], + lastAction: 'RESET', + }; + case 'ADD': + return { + ...state, + count: state.count + action.payload, + history: [...state.history, state.count + action.payload], + lastAction: `ADD ${action.payload}`, + }; + case 'SET_STATE': + return { + ...action.payload, + lastAction: 'SET_STATE', + }; + default: + return state; + } +} + +function secondaryCounterReducer( + state: SecondaryCounterState, + action: SecondaryCounterAction, +): SecondaryCounterState { + switch (action.type) { + case 'MULTIPLY': + return { + ...state, + count: state.count * state.multiplier, + history: [...state.history, state.count * state.multiplier], + lastOperation: `Multiplied by ${state.multiplier}`, + }; + case 'DIVIDE': + return { + ...state, + count: state.count / state.multiplier, + history: [...state.history, state.count / state.multiplier], + lastOperation: `Divided by ${state.multiplier}`, + }; + case 'SET_MULTIPLIER': + return { + ...state, + multiplier: action.payload, + history: [...state.history], + lastOperation: `Set multiplier to ${action.payload}`, + }; + case 'RESET': + return { + count: 0, + multiplier: 2, + history: [], + lastOperation: 'Reset', + }; + case 'SET_STATE': + return { + ...action.payload, + lastOperation: 'SET_STATE', + }; + default: + return state; + } +} + +function FunctionalReducerCounter({ + initialCount = 0, + step = 1, + title = 'Function-based Reducer Counter', + theme = { + backgroundColor: '#ffffff', + textColor: '#330002', + }, +}: CounterProps): JSX.Element { + const [clickCount, setClickCount] = useState(0); + const [lastClickTime, setLastClickTime] = useState(null); + const [averageTimeBetweenClicks, setAverageTimeBetweenClicks] = useState(0); + + const [state, dispatch] = useReducer( + (state: CounterState, action: CounterAction) => counterReducer(state, action, step), + { + count: initialCount, + history: [], + lastAction: 'none', + }, + ); + + const [secondaryState, secondaryDispatch] = useReducer(secondaryCounterReducer, { + count: initialCount, + multiplier: 2, + history: [], + lastOperation: 'none', + }); + + return ( +
+

{title}

+ +
+

Primary Counter: {state.count}

+
+ +
+ + + + + +
+ +
+

History:

+
+ {state.history.map((value, index) => ( + + {value} + {index < state.history.length - 1 ? ' β†’ ' : ''} + + ))} +
+
+ +
+

Secondary Counter: {secondaryState.count}

+
+ + + + +
+
+

Current Multiplier: {secondaryState.multiplier}

+

History:

+
+ {secondaryState.history.map((value, index) => ( + + {value} + {index < secondaryState.history.length - 1 ? ' β†’ ' : ''} + + ))} +
+
+
+
+ ); +} + +export default FunctionalReducerCounter; diff --git a/demo-app/src/client/Components/FunctionalStateCounter.tsx b/demo-app/src/client/Components/FunctionalStateCounter.tsx new file mode 100644 index 000000000..a49916177 --- /dev/null +++ b/demo-app/src/client/Components/FunctionalStateCounter.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; + +type CounterProps = { + initialCount?: number; + step?: number; + title?: string; + theme?: { + backgroundColor?: string; + textColor?: string; + }; +}; + +function FunctionalStateCounter({ + initialCount = 0, + step = 1, + title = 'Function-based State Counter', + theme = { + backgroundColor: '#ffffff', + textColor: '#330002', + }, +}: CounterProps): JSX.Element { + const [count, setCount] = useState(initialCount); + const [history, setHistory] = useState([]); + const [lastAction, setLastAction] = useState('none'); + + const handleAction = (type: string, payload?: number) => { + let newCount = count; + switch (type) { + case 'INCREMENT': + newCount = count + step; + setCount(newCount); + setHistory([...history, newCount]); + setLastAction('INCREMENT'); + break; + case 'DECREMENT': + newCount = count - step; + setCount(newCount); + setHistory([...history, newCount]); + setLastAction('DECREMENT'); + break; + case 'DOUBLE': + newCount = count * 2; + setCount(newCount); + setHistory([...history, newCount]); + setLastAction('DOUBLE'); + break; + case 'ADD': + newCount = count + (payload || 0); + setCount(newCount); + setHistory([...history, newCount]); + setLastAction(`ADD ${payload}`); + break; + case 'RESET': + setCount(0); + setHistory([]); + setLastAction('RESET'); + break; + } + }; + + return ( +
+

{title}

+
+

Current Count: {count}

+
+ +
+ + + + + +
+ +
+

History:

+
+ {history.map((value, index) => ( + + {value} + {index < history.length - 1 ? ' β†’ ' : ''} + + ))} +
+
+
+ ); +} + +export default FunctionalStateCounter; diff --git a/demo-app/src/client/Components/Home.tsx b/demo-app/src/client/Components/Home.tsx index cafff52ef..9952f9e29 100644 --- a/demo-app/src/client/Components/Home.tsx +++ b/demo-app/src/client/Components/Home.tsx @@ -1,25 +1,60 @@ import React from 'react'; +import { useTheme } from '../../contexts/ThemeContext'; +import { useAuth } from '../../contexts/AuthContext'; function Home(): JSX.Element { + const { theme } = useTheme(); + const { user, login, logout } = useAuth(); + return ( -
-

REACTIME - DEMO APP

-

- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." -

-

- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." -

+
+

REACTIME - DEMO APP

+ + {user ? ( +
+

Welcome, {user.username}!

+ +
+ ) : ( +
+

Please log in:

+ + +
+ )}
); } - export default Home; diff --git a/demo-app/src/client/Components/Increment.tsx b/demo-app/src/client/Components/Increment.tsx deleted file mode 100644 index 3836152a7..000000000 --- a/demo-app/src/client/Components/Increment.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { useState } from 'react'; -function Increment(): JSX.Element { - const [count, setCount] = useState(0); - return ( -
- -
- ); -} - -export default Increment; diff --git a/demo-app/src/client/Components/IncrementWithMoreHooks.jsx b/demo-app/src/client/Components/IncrementWithMoreHooks.jsx deleted file mode 100644 index 4cba379b3..000000000 --- a/demo-app/src/client/Components/IncrementWithMoreHooks.jsx +++ /dev/null @@ -1,312 +0,0 @@ -import React, { - useState, - useEffect, - useContext, - useReducer, - useRef, - useMemo, - useCallback, -} from 'react'; -import { userContext, tripContext } from './ButtonsWithMoreHooks'; -import { useImmer } from 'use-immer'; - -/** - * This component as well as ButtonsWithMoreHooks were made to show where data for different - * hooks show up in the react fiber tree. There are duplicates of most hooks so you can see - * how the react fiber tree iterates over hook data. - */ - -/** - * This file is a JSX file, not a TSX file, on purpose. The code won't be converted to common JS - * before being bundled by webpack. There were some errors that weren't showing up for the other - * Increment.tsx file based on how webpack uglifies ES6 files. Maintaining this as a JSX file - * will help check for these types of issues. - */ - -/** - * ES5 function definitions are intentionally used for most functions as ES6 function definitions were one - * of the reasons Reactime 18 was breaking. Handleclick within the component definitions is ES6 to cover - * this case, but having only 1 ES6 function definition in the file makes it easy to switch for testing - * purposes. Functional component definitions made with ES6 - */ - -//---------useReducer reducer and initialState functions------------------------------------------------------------------ - -function reducer1(state, action) { - if (action.type === 'INCREMENT_UR1COUNT') { - return { - UR1Count: state.UR1Count + 1, - }; - } - // This throws an error if you send a dispatch action to this reducer without having - // the matching action.type string, rather than returning the initial state. This'll - // help display unexpected activity. - throw Error('Unknown action 1.'); -} - -function reducer2(state, action) { - if (action.type === 'ADD_TO_ARRAY') { - return { - UR2Array: state.UR2Array.concat(action.payload), - }; - } - // This throws an error if you send a dispatch action to this reducer without having - // the matching action.type string, rather than returning the initial state. This'll - // help display unexpected activity. - throw Error('Unknown action 2.'); -} - -function createInitialState(num) { - return { UR1Count: num }; -} - -//---------Custom Hook Definition----------------------------------------------------------------------------------------- - -function useCustomHook() { - const [customCount, setCustomCount] = useState(0); - - useEffect(() => { - setCustomCount((customCount) => customCount + 1); - }, []); - - return { customCount }; -} - -//-------IncrementWithMoreHooks Component Definition-------------------------------------------------------------------------- - -const IncrementWithMoreHooks = () => { - //--------useState Invocations---------------------------------------------------------------------------------------------- - - // useState stores stateful data in the component it's defined in - const [count, setCount] = useState(0); - const [buttonState, setButtonState] = useState(false); - - //--------useContext Invocations-------------------------------------------------------------------------------------------- - - // useContext accesses stateful data defined in a 'super-parent' (grandparent, great-grandparent, etc.) component - // that the current component is nested in, regardless of how many component parents are between the - // current component and that 'super' parent component, allowing the user to avoid prop drilling through - // multiple parent-child component relationships. - const { user, setUser } = useContext(userContext); - const { trip } = useContext(tripContext); - - //--------useReduce Invocations---------------------------------------------------------------------------------------------- - - // useReducer stores stateful data in the component it's defined in. - // It also takes custom 'reducer' functions which dictate how the stateful - // data is updated when particular arguments are provided to invoked 'dispatch' functions - const [UR1State, dispatch1] = useReducer(reducer1, 0, createInitialState); - const [UR2State, dispatch2] = useReducer(reducer2, { UR2Array: [] }); - - //--------useRef Invocations------------------------------------------------------------------------------------------------- - - // useRef stores stateful data that’s not needed for rendering. - // This is generally used to hold DOM elements that the user wants to manipulate. - const ref1 = useRef(null); - const ref2 = useRef(1); - - //--------useMemo Invocations------------------------------------------------------------------------------------------------ - - // useMemo takes a callback and dependency arrays holding values used in the callback. - // It'll cache results of calling the callback for different values of the dependency and - // if it sees the same value again it'll grab the result from the cache rather than calling - // the callback again. This'll save resources if calling the callback is expensive. - const memoValue1 = useMemo(() => count * ref2.current * 2, [count, ref2.current]); - const memoValue2 = useMemo(() => count * 2, [count]); - - //--------useCallback Invocations------------------------------------------------------------------------------------------- - - // useCallback is similar to useMemo, however it caches a function definition that changes - // based on the dependencies. This'll save resources when defining said function is expensive. - const callback1 = useCallback(() => { - return count * memoValue1; - }, [count, memoValue1]); - const callback2 = useCallback(() => { - return count * memoValue2; - }, [count, memoValue2]); - - //--------useEffect Invocations--------------------------------------------------------------------------------------------- - - // useEffect is a lifecycle hook that invokes its first argument 'setup' callback based on whether or not - // the contents of the second argument's 'dependencies' array have changed from the previous render. - // - // Not including a second argument will invoke 'setup' every time the component renders. - // An empty dependency array will invoke 'setup' only for the first component render. - // Providing variables to the dependency array will invoke 'setup' on the first component - // and every subsequent render where the variable has changed. - // Returning a callback will execute the callback when the current component unmounts - useEffect(() => { - setUser('Mark'); - return () => { - console.log('I get executed when IncrementWithMoreHooks unmounts'); - }; - }, []); - - useEffect(() => { - setCount((count) => count + 1); - }, [buttonState]); - - //--------useImmer Invocation------------------------------------------------------------------------------------------------ - - // use-immer is an npm library used for writing simple immutable changes with mutable syntax - const [person, updatePerson] = useImmer({ - name: 'Jeff', - age: 34, - }); - - // function to increment useImmer age value - function incrementPersonAge() { - updatePerson((draft) => { - draft.age++; - }); - } - - //--------Custom Hook Invocation--------------------------------------------------------------------------------------------- - - // Custom hook implementation - const { customCount } = useCustomHook(); - - //--------Other function definitions--------------------------------------------------------------------------------------------- - - //Click function to alter state of buttonState - const handleClick = () => { - if (buttonState) { - setButtonState(false); - } else { - setButtonState(true); - } - }; - - // Converts values like functions, objects, and 'false 'booleans that wouldn't otherwise print in a JSX element to strings if possible - function printString(val) { - let result; - - if (typeof val === 'function') { - return val.toString(); - } - - try { - result = JSON.stringify(val); - } catch { - result = 'Failed to convert into JSON string'; - } - return result; - } - - //--------Return Statement------------------------------------------------------------------------------------------------------ - - return ( -
-

- Increment With More Hooks -

- -
-

useState data:

-

- count: {printString(count)} -
- count will increment via useEffect on buttonState state change -

-

buttonState: {printString(buttonState)}

- -
- -
-

useContext data:

-

user: {printString(user)}

-

trip: {printString(trip)}

-
- -
-

useReducer data:

-

UR1State: {printString(UR1State)}

- -

UR2State: {printString(UR2State)}

- -
- -
-

useRef data:

-

ref1: {ref1.current ? "holds ref to element with id 'useRef-data'" : 'null'}

-

- ref2: {printString(ref2)} -
- Notice that this change doesn't cause the page to re-render -

- -
- -
-

useMemo data:

-

- memoValue1: {printString(memoValue1)} -
- memoValue1 depends on useState "count" and useRef "ref2" -

-

- memoValue2: {printString(memoValue2)} -
- memoValue2 depends on useState "count" -

-
- -
-

useCallback data:

-

callback1: {printString(callback1)}

-

callback2: {printString(callback2)}

-
- -
-

useEffect data:

-

- One useEffect sets useContext's user to {printString(user)} after the component's first - render -

-

One useEffect increments useState's count every time buttonState's state changes

-

- count: {printString(count)} -
- buttonState: {printString(buttonState)} -

- -
- -
-

useImmer data:

-

person: {printString(person)}

- -
- -
-

useCustomHook data:

-

customCount: {printString(customCount)}

-

- The custom hook utilizes useEffect to increments a useState var customCount only on the - first render. Remove the empty dependency array from the second parameter of the useEffect - call to see a show! -

-
-
- ); -}; - -export default IncrementWithMoreHooks; diff --git a/demo-app/src/client/Components/Nav.tsx b/demo-app/src/client/Components/Nav.tsx index c289f8d81..274c5ae39 100644 --- a/demo-app/src/client/Components/Nav.tsx +++ b/demo-app/src/client/Components/Nav.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import ThemeToggle from './ThemeToggle'; function Nav(): JSX.Element { return ( @@ -11,8 +12,12 @@ function Nav(): JSX.Element { Tic-Tac-Toe - Counter + State Counter + + Reducer Counter + +
); } diff --git a/demo-app/src/client/Components/ReducerCounter.tsx b/demo-app/src/client/Components/ReducerCounter.tsx new file mode 100644 index 000000000..65b5d6155 --- /dev/null +++ b/demo-app/src/client/Components/ReducerCounter.tsx @@ -0,0 +1,141 @@ +import React, { Component } from 'react'; + +type CounterProps = { + initialCount?: number; + step?: number; + title?: string; + theme?: { + backgroundColor?: string; + textColor?: string; + }; +}; + +type CounterState = { + count: number; + history: number[]; + lastAction: string; +}; + +type CounterAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'DOUBLE' } + | { type: 'RESET' } + | { type: 'ADD'; payload: number }; + +class ReducerCounter extends Component { + static defaultProps = { + initialCount: 0, + step: 1, + title: 'Class-based Reducer Counter', + theme: { + backgroundColor: '#ffffff', + textColor: '#330002', + }, + }; + + static initialState(initialCount: number): CounterState { + return { + count: initialCount, + history: [], + lastAction: 'none', + }; + } + + static reducer(state: CounterState, action: CounterAction, step: number): CounterState { + switch (action.type) { + case 'INCREMENT': + return { + ...state, + count: state.count + step, + history: [...state.history, state.count + step], + lastAction: 'INCREMENT', + }; + case 'DECREMENT': + return { + ...state, + count: state.count - step, + history: [...state.history, state.count - step], + lastAction: 'DECREMENT', + }; + case 'DOUBLE': + return { + ...state, + count: state.count * 2, + history: [...state.history, state.count * 2], + lastAction: 'DOUBLE', + }; + case 'RESET': + return { + ...ReducerCounter.initialState(0), + lastAction: 'RESET', + }; + case 'ADD': + return { + ...state, + count: state.count + action.payload, + history: [...state.history, state.count + action.payload], + lastAction: `ADD ${action.payload}`, + }; + default: + return state; + } + } + + constructor(props: CounterProps) { + super(props); + this.state = ReducerCounter.initialState(props.initialCount || 0); + this.dispatch = this.dispatch.bind(this); + } + + dispatch(action: CounterAction): void { + this.setState((currentState) => + ReducerCounter.reducer(currentState, action, this.props.step || 1), + ); + } + + render(): JSX.Element { + const { title, theme } = this.props; + + return ( +
+

{title}

+
+

Current Count: {this.state.count}

+
+ +
+ + + + + +
+ +
+

History:

+
+ {this.state.history.map((value, index) => ( + + {value} + {index < this.state.history.length - 1 ? ' β†’ ' : ''} + + ))} +
+
+
+ ); + } +} + +export default ReducerCounter; diff --git a/demo-app/src/client/Components/ThemeToggle.tsx b/demo-app/src/client/Components/ThemeToggle.tsx new file mode 100644 index 000000000..81f203747 --- /dev/null +++ b/demo-app/src/client/Components/ThemeToggle.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useTheme } from '../../contexts/ThemeContext'; + +const ThemeToggle = (): JSX.Element => { + const { theme, toggleTheme } = useTheme(); + const isDark = theme.backgroundColor === '#1a202c'; + + return ( + + ); +}; + +export default ThemeToggle; diff --git a/demo-app/src/client/Router.tsx b/demo-app/src/client/Router.tsx index 47dc6e14b..619dfce8b 100644 --- a/demo-app/src/client/Router.tsx +++ b/demo-app/src/client/Router.tsx @@ -1,31 +1,59 @@ +// src/client/Router.tsx import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import { createRoot } from 'react-dom/client'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { ThemeProvider } from '../contexts/ThemeContext'; +import { AuthProvider } from '../contexts/AuthContext'; import Nav from './Components/Nav'; import Board from './Components/Board'; import Home from './Components/Home'; import Buttons from './Components/Buttons'; -// import ButtonsWithMoreHooks from './Components/ButtonsWithMoreHooks'; +import ReducerCounter from './Components/ReducerCounter'; +import FunctionalReducerCounter from './Components/FunctionalReducerCounter'; +import FunctionalStateCounter from './Components/FunctionalStateCounter'; const domNode = document.getElementById('root'); +if (!domNode) throw new Error('Root element not found'); const root = createRoot(domNode); -root.render( - -