Skip to content

Commit 41ce197

Browse files
authored
Merge pull request jherr#13 from craig-waite/main
Added an extended demo option
2 parents 9d14bb1 + 6a08190 commit 41ce197

23 files changed

+1018
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
package-lock.json
12+
dist
13+
dist-ssr
14+
*.local
15+
16+
# Editor directories and files
17+
.vscode/*
18+
!.vscode/extensions.json
19+
.idea
20+
.DS_Store
21+
*.suo
22+
*.ntvs*
23+
*.njsproj
24+
*.sln
25+
*.sw?
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "simple-context",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"react": "^18.2.0",
13+
"react-dom": "^18.2.0"
14+
},
15+
"devDependencies": {
16+
"@types/react": "^18.0.17",
17+
"@types/react-dom": "^18.0.6",
18+
"@vitejs/plugin-react": "^2.1.0",
19+
"typescript": "^4.6.4",
20+
"vite": "^3.1.0"
21+
}
22+
}
Loading
Lines changed: 1 addition & 0 deletions
Loading

fast-context-generic-extended/src/App.css

Whitespace-only changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import createFastContext from "./app/createFastContext";
2+
import MainPage from "./pages/MainPage";
3+
4+
export const {
5+
FastContextProvider:AppFastContextProvider,
6+
useFastContextFields:useAppFastContextFields
7+
} = createFastContext({
8+
first: "" as string,
9+
last: "" as string,
10+
});
11+
12+
function App() {
13+
console.log(`App Rendering`)
14+
return (
15+
<AppFastContextProvider>
16+
<div className="container">
17+
<img src="../public/DevToolsImage.png" style={{float: 'right', width: '350px'}} alt="Dev Tools"/>
18+
<h1>App (NO RE-RENDERS)</h1>
19+
<p>The App's Fast Context is created and exported here, but could be created in any file.</p>
20+
<p>Switch on the re-render highlight function (see image) in dev tools to see when re-renders happen.</p>
21+
<MainPage />
22+
</div>
23+
</AppFastContextProvider>
24+
);
25+
}
26+
27+
export default App;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, {
2+
useRef,
3+
createContext,
4+
useContext,
5+
useCallback,
6+
useSyncExternalStore,
7+
} from "react";
8+
9+
export default function createFastContext<FastContext>(initialState: FastContext) {
10+
function useFastContextData(): {
11+
get: () => FastContext;
12+
set: (value: Partial<FastContext>) => void;
13+
subscribe: (callback: () => void) => () => void;
14+
} {
15+
const store = useRef(initialState);
16+
17+
const get = useCallback(() => store.current, []);
18+
19+
const subscribers = useRef(new Set<() => void>());
20+
21+
const set = useCallback((value: Partial<FastContext>) => {
22+
store.current = { ...store.current, ...value };
23+
subscribers.current.forEach((callback) => callback());
24+
}, []);
25+
26+
const subscribe = useCallback((callback: () => void) => {
27+
subscribers.current.add(callback);
28+
return () => subscribers.current.delete(callback);
29+
}, []);
30+
31+
return {
32+
get,
33+
set,
34+
subscribe,
35+
};
36+
}
37+
38+
type UseFastContextDataReturnType = ReturnType<typeof useFastContextData>;
39+
40+
const FastContext = createContext<UseFastContextDataReturnType | null>(null);
41+
42+
function FastContextProvider({ children }: Readonly<{ children: React.ReactNode }>) {
43+
return (
44+
<FastContext.Provider value={useFastContextData()}>
45+
{children}
46+
</FastContext.Provider>
47+
);
48+
}
49+
50+
function useFastContext<SelectorOutput>(
51+
selector: (store: FastContext) => SelectorOutput
52+
): [SelectorOutput, (value: Partial<FastContext>) => void] {
53+
const fastContext = useContext(FastContext);
54+
if (!fastContext) {
55+
throw new Error("Store not found");
56+
}
57+
58+
const state = useSyncExternalStore(
59+
fastContext.subscribe,
60+
() => selector(fastContext.get()),
61+
() => selector(initialState),
62+
);
63+
64+
return [state, fastContext.set];
65+
}
66+
67+
function useFastContextFields<SelectorOutput>(
68+
fieldNames: string[]
69+
): { [key: string]: { get: SelectorOutput, set: (value: any) => void } } {
70+
const gettersAndSetters: { [key: string]: { get: SelectorOutput, set: (value: any) => void } } = {};
71+
for (const fieldName of fieldNames) {
72+
const [getter, setter] = useFastContext((fc) => (fc as Record<string, SelectorOutput>)[fieldName]);
73+
gettersAndSetters[fieldName] = { get: getter, set: (value: any) => setter({ [fieldName]: value } as Partial<FastContext>) };
74+
}
75+
76+
return gettersAndSetters;
77+
}
78+
79+
return {
80+
FastContextProvider,
81+
useFastContextFields,
82+
};
83+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { PropDrivenDisplayContainer, SelfDrivenDisplayContainer } from "./DisplayContainer";
2+
import { PropDrivenFormContainer, SelfDrivenFormContainer } from "./FormContainer";
3+
4+
export function PropDrivenContentContainer() {
5+
console.log(`Prop Driven Content Rendering`)
6+
return (
7+
<div className="container">
8+
<h3>'Prop Driven' Content Container (NO RE-RENDERS)</h3>
9+
<PropDrivenFormContainer />
10+
<PropDrivenDisplayContainer />
11+
</div>
12+
);
13+
};
14+
15+
export function SelfDrivenContentContainer() {
16+
console.log(`Self Driven Content Rendering`)
17+
return (
18+
<div className="container">
19+
<h3>'Self Driven' Content Container (NO RE-RENDERS)</h3>
20+
<SelfDrivenFormContainer />
21+
<SelfDrivenDisplayContainer />
22+
</div>
23+
);
24+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useAppFastContextFields } from "../App";
2+
3+
type Props = {
4+
fieldName?: string;
5+
label?: string;
6+
value?: string;
7+
};
8+
export function PropDrivenDisplay({label, value}: Readonly<Props>) {
9+
console.log(`Prop Driven ${label} display rendering`)
10+
return (
11+
<div className="value">
12+
{label ? <label>{label} : </label> : null}
13+
<input value={value} readOnly style={{backgroundColor: "#eee", cursor: 'auto', border: 0}}/>
14+
</div>
15+
);
16+
};
17+
18+
export function SelfDrivenDisplay({ fieldName = "", label }: Readonly<Props>) {
19+
console.log(`Self Driven ${label} display rendering`)
20+
const value = useAppFastContextFields([fieldName]);
21+
return (
22+
<div className="value">
23+
{label ? <label>{label} : </label> : null}
24+
<input value={value[fieldName].get as string} readOnly style={{backgroundColor: "#eee", cursor: 'auto', border: 0}}/>
25+
</div>
26+
);
27+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useAppFastContextFields } from "../App";
2+
import { PropDrivenDisplay, SelfDrivenDisplay } from "./Display";
3+
4+
export function PropDrivenDisplayContainer() {
5+
console.log(`Prop Driven Display Rendering`)
6+
const fields = useAppFastContextFields(['first', 'last']);
7+
return (
8+
<div className="container">
9+
<h4>'Prop Driven' Display (Container AND children re-render on field changes)</h4>
10+
<PropDrivenDisplay label="First Name" value={fields.first.get as string} />
11+
<PropDrivenDisplay label="Last Name" value={fields.last.get as string} />
12+
</div>
13+
);
14+
};
15+
16+
export function SelfDrivenDisplayContainer() {
17+
console.log(`Self Driven Display Rendering`)
18+
return (
19+
<div className="container">
20+
<h4>'Self Driven' Display (NO RE-RENDERS - only children re-render on field changes)</h4>
21+
<SelfDrivenDisplay fieldName="first" label="First Name" />
22+
<SelfDrivenDisplay fieldName="last" label="Last Name" />
23+
</div>
24+
);
25+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useAppFastContextFields } from "../App";
2+
import { FormDrivenTextInput, SelfDrivenTextInput } from "./TextInput";
3+
4+
export function PropDrivenFormContainer() {
5+
console.log(`Prop Driven Form Rendering`)
6+
const fields = useAppFastContextFields(['first', 'last']);
7+
console.log(`fields:`, fields)
8+
return (
9+
<div className="container">
10+
<h4>'Prop Driven' Input Form (Form AND children re-render on field changes)</h4>
11+
<FormDrivenTextInput value={fields.first.get as string} label='First Name' onChange={fields.first.set}/>
12+
<FormDrivenTextInput value={fields.last.get as string} label='Last Name' onChange={fields.last.set} />
13+
</div>
14+
);
15+
};
16+
17+
export function SelfDrivenFormContainer() {
18+
console.log(`Self Driven Form Rendering`)
19+
return (
20+
<div className="container">
21+
<h4>'Self Driven' Input Form (NO RE-RENDERS - only children re-render on field changes)</h4>
22+
<SelfDrivenTextInput fieldName="first" label='First Name'/>
23+
<SelfDrivenTextInput fieldName="last" label='Last Name'/>
24+
</div>
25+
);
26+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useAppFastContextFields } from "../App";
2+
3+
type Props = {
4+
fieldName?: string;
5+
label?: string;
6+
value?: string;
7+
onChange?: (value: string) => void;
8+
};
9+
export function FormDrivenTextInput( { label = '', value, onChange = (v) => {}}: Readonly<Props> ) {
10+
console.log(`Prop Driven ${label} input rendering`)
11+
return (
12+
<div className="field">
13+
{label ? <label>{label} : </label> : null}
14+
<input
15+
value={value}
16+
onChange={(e) => onChange(e.target.value)}
17+
/>
18+
</div>
19+
);
20+
};
21+
22+
23+
export function SelfDrivenTextInput( { fieldName = "", label }: Readonly<Props> ) {
24+
console.log(`Self Driven ${label} input rendering`)
25+
const field = useAppFastContextFields([fieldName]);
26+
return (
27+
<div className="field">
28+
{label ? <label>{label} : </label> : null}
29+
<input
30+
value={field[fieldName].get as string}
31+
onChange={(e) => field[fieldName].set(e.target.value)}
32+
/>
33+
</div>
34+
);
35+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
body {
2+
font-family: Arial, Helvetica, sans-serif;
3+
padding: 1rem;
4+
}
5+
6+
.container {
7+
margin-top: 0.5rem;
8+
padding: 0.5rem 1.5rem;
9+
border: 1px solid #ccc;
10+
}
11+
12+
.field, .value {
13+
padding: 0.5rem;
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
import App from './App'
4+
import './index.css'
5+
6+
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
7+
<React.StrictMode>
8+
<App />
9+
</React.StrictMode>
10+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { PropDrivenContentContainer, SelfDrivenContentContainer } from "../components/ContentContainer";
2+
3+
export default function MainPage() {
4+
console.log(`Page Rendering`)
5+
return (
6+
<div className="container">
7+
<h2>Page (NO RE-RENDERS)</h2>
8+
<div style={{display: 'flex', justifyContent: 'space-around', margin: '1em'}}>
9+
<PropDrivenContentContainer />
10+
<SelfDrivenContentContainer />
11+
</div>
12+
</div>
13+
);
14+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"useDefineForClassFields": true,
5+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
6+
"allowJs": false,
7+
"skipLibCheck": true,
8+
"esModuleInterop": false,
9+
"allowSyntheticDefaultImports": true,
10+
"strict": true,
11+
"forceConsistentCasingInFileNames": true,
12+
"module": "ESNext",
13+
"moduleResolution": "Node",
14+
"resolveJsonModule": true,
15+
"isolatedModules": true,
16+
"noEmit": true,
17+
"jsx": "react-jsx"
18+
},
19+
"include": ["src"],
20+
"references": [{ "path": "./tsconfig.node.json" }]
21+
}

0 commit comments

Comments
 (0)