({
46 | ball1: 'red',
47 | ball2: 'green',
48 | ball3: 'red',
49 | ball4: 'green',
50 | ball5: 'red',
51 | ball6: 'green',
52 | });
53 | const handleClick = () => {
54 | setValue({
55 | ball1: toggleColor(value.ball1),
56 | ball2: value.ball2,
57 | ball3: toggleColor(value.ball3),
58 | ball4: value.ball4,
59 | ball5: toggleColor(value.ball5),
60 | ball6: value.ball6,
61 | });
62 | };
63 | const handleOddClick = () => {
64 | setValue({
65 | ball1: value.ball1,
66 | ball2: toggleColor(value.ball2),
67 | ball3: value.ball3,
68 | ball4: toggleColor(value.ball4),
69 | ball5: value.ball5,
70 | ball6: toggleColor(value.ball6),
71 | });
72 | };
73 | return (
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 |
82 | function AppContent() {
83 | return (
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | }
94 |
95 | const AppContentMemo = React.memo(AppContent);
96 |
97 | function Ball1() {
98 | const ball1Color = useBall1();
99 |
100 | return ;
101 | }
102 |
103 | function Ball2() {
104 | const ball2Color = useBall2();
105 |
106 | return ;
107 | }
108 |
109 | function Ball3() {
110 | const ball3Color = useBall3();
111 |
112 | return ;
113 | }
114 |
115 | function Ball4() {
116 | const ball4Color = useBall4();
117 |
118 | return ;
119 | }
120 |
121 | function Ball5() {
122 | const ball5Color = useBall5();
123 |
124 | return ;
125 | }
126 |
127 | function Ball6() {
128 | const ball6Color = useBall6();
129 |
130 | return ;
131 | }
132 |
133 | export default App;
134 |
--------------------------------------------------------------------------------
/examples/basic/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 | import reportWebVitals from './reportWebVitals';
5 |
6 | const root = ReactDOM.createRoot(
7 | document.getElementById('root') as HTMLElement
8 | );
9 | root.render(
10 |
11 |
12 |
13 | );
14 |
15 | // If you want to start measuring performance in your app, pass a function
16 | // to log results (for example: reportWebVitals(console.log))
17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
18 | reportWebVitals();
19 |
--------------------------------------------------------------------------------
/examples/basic/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/basic/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/examples/basic/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/examples/basic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-next-context",
3 | "description": "Performance optimized React Context API.",
4 | "version": "0.1.5",
5 | "main": "dist/index.js",
6 | "module": "./dist/esm/index.js",
7 | "devDependencies": {
8 | "@testing-library/jest-dom": "^5.16.2",
9 | "@testing-library/react": "^12.1.2",
10 | "@testing-library/user-event": "^13.5.0",
11 | "@types/node": "^17.0.14",
12 | "@types/react": "^17.0.39",
13 | "@types/react-dom": "^17.0.11",
14 | "@types/styled-components": "^5.1.22",
15 | "cross-env": "^7.0.3",
16 | "webpack": "^5.68.0",
17 | "rollup": "^2.67.2",
18 | "rollup-plugin-typescript2": "^0.31.2",
19 | "typescript": "^4.5.5"
20 | },
21 | "peerDependencies": {
22 | "react": "^16 || ^17 || ^18"
23 | },
24 | "scripts": {
25 | "typecheck": "tsc",
26 | "build": "cross-env NODE_ENV=production tsc --noEmit --project ./tsconfig.json && rollup -c"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "author": {
47 | "name": "Ahmed Bouhuolia",
48 | "email": "a.bouhuolia@gmail.com"
49 | }
50 | }
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from 'rollup-plugin-typescript2';
2 | import pkg from './package.json';
3 |
4 | const external = ['react'];
5 |
6 | const config = [
7 | {
8 | input: 'src/index.ts',
9 | plugins: [
10 | typescript({
11 | tsconfig: 'tsconfig.json',
12 | }),
13 | ],
14 | external: external.concat(Object.keys(pkg.dependencies || [])),
15 | output: [
16 | { dir: './dist', format: 'cjs', sourcemap: true },
17 | {
18 | dir: './dist/esm',
19 | format: 'es',
20 | sourcemap: true,
21 | preserveModules: true,
22 | },
23 | ],
24 | },
25 | ];
26 |
27 | export default config;
28 |
--------------------------------------------------------------------------------
/src/ContextNextProvider.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement,
4 | useEffect,
5 | useRef,
6 | Provider,
7 | ReactNode,
8 | } from 'react';
9 | import { ContextNextValue, Listener, NextContext } from './types';
10 |
11 | const PROVIDER_NAME = '@use-context-next';
12 | const ORIGINAL_PROVIDER = Symbol();
13 |
14 | export const createNextContext = (defaultValue: Value) => {
15 | const Context = createContext>({
16 | value: defaultValue,
17 | listeners: new Set(),
18 | } as ContextNextValue);
19 |
20 | const NextContext = Context as unknown as NextContext;
21 |
22 | (NextContext as any).Provider = createNextProvider(Context.Provider);
23 | (NextContext as any)[ORIGINAL_PROVIDER] = Context.Provider;
24 |
25 | NextContext.displayName = PROVIDER_NAME;
26 |
27 | return NextContext;
28 | };
29 |
30 | const createNextProvider = (
31 | ReactProvider: Provider>
32 | ) => {
33 | const ContextProvider = ({
34 | children,
35 | value,
36 | }: {
37 | children: ReactNode;
38 | value: Value;
39 | }) => {
40 | const listeners = new Set();
41 | const contextValue = useRef>({ value, listeners });
42 |
43 | const triggerListeners = () => {
44 | if (!contextValue.current) return;
45 |
46 | contextValue.current.listeners.forEach((listener) => {
47 | listener({ value });
48 | });
49 | };
50 | useEffect(() => {
51 | contextValue.current.value = value;
52 | triggerListeners();
53 | }, [value]);
54 |
55 | return createElement(
56 | ReactProvider,
57 | { value: contextValue.current },
58 | children
59 | );
60 | };
61 | ContextProvider.displayName = PROVIDER_NAME;
62 | return ContextProvider;
63 | };
64 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ContextNextProvider';
2 | export * from './useContextSelector';
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, Context } from 'react';
2 |
3 | export type Listener = ({ value }: any) => void;
4 | export type Listeners = Set;
5 | export type ContextNextValue = {
6 | value: T;
7 | listeners: Listeners;
8 | };
9 |
10 | export type NextContextProvider = (props: {
11 | children: ReactNode;
12 | value: T;
13 | }) => React.FunctionComponentElement>>;
14 |
15 | export interface NextContext extends Omit, 'Provider'> {
16 | Provider: NextContextProvider;
17 | ORIGINAL_PROVIDER: NextContextProvider;
18 | }
19 |
--------------------------------------------------------------------------------
/src/useContextSelector.ts:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import { ContextNextValue, NextContext } from './types';
3 | import { useIsomorphicLayoutEffect } from './utils';
4 |
5 | export const useContextSelector = (
6 | context: NextContext,
7 | selector: (value: Value) => Output,
8 | comparator: (value1: any, value2: any) => boolean = Object.is
9 | ): Output => {
10 | const contextValue = useContext>(
11 | context as unknown as React.Context>
12 | );
13 | const initialValue = selector(contextValue.value);
14 | const [state, setState] = useState