Skip to content

Commit

Permalink
fix: ensure enhanced value is preserved
Browse files Browse the repository at this point in the history
  • Loading branch information
iamogbz committed Sep 21, 2020
1 parent 6687a3f commit 23d644e
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 152 deletions.
86 changes: 86 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module.exports = {
env: {
browser: true,
es6: true,
jest: true,
node: true,
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:jest/all",
"plugin:prettier/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier/@typescript-eslint",
],
globals: {
browser: false,
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 6,
sourceType: "module",
},
rules: {
"@typescript-eslint/no-unused-vars": [
1,
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"arrow-body-style": [2, "as-needed"],
"class-methods-use-this": 0,
"jest/no-hooks": 0,
"jest/prefer-expect-assertions": 0,
"no-console": 1,
"no-param-reassign": ["error", { props: false }],
"no-trailing-spaces": 2,
"no-unused-vars": "off",
"no-use-before-define": "off",
"object-curly-newline": [
2,
{
consistent: true,
multiline: true,
},
],
"object-property-newline": [
2,
{
allowAllPropertiesOnSameLine: true,
},
],
"prettier/prettier": [
2,
{
bracketSpacing: true,
jsxBracketSameLine: false,
semi: true,
singleQuote: false,
tabWidth: 4,
trailingComma: "all",
},
],
"react/jsx-first-prop-new-line": [2, "multiline"],
"react/jsx-indent": [2, 4],
"react/jsx-indent-props": [2, 4],
"sort-imports": [
2,
{
ignoreDeclarationSort: true,
},
],
"sort-keys": "warn",
},
settings: {
"import/resolver": {
node: {
paths: ["src"],
},
},
react: {
version: "detect",
},
},
};
70 changes: 0 additions & 70 deletions .eslintrc.json

This file was deleted.

1 change: 0 additions & 1 deletion .prettierrc.toml

This file was deleted.

53 changes: 38 additions & 15 deletions src/components/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as React from "react";
import { ActionTypes } from "../utils/actionTypes";
import { createAction } from "../createAction";
import { useGetter } from "../hooks/useGetter";
import { useAccessor } from "../hooks/useAccessor";
import { useObservable } from "../hooks/useObservable";
import { useOnce } from "../hooks/useOnce";

export function Provider<S, T extends string, P>({
children,
Expand All @@ -12,7 +11,7 @@ export function Provider<S, T extends string, P>({
const root = React.useContext(Context);

const [state, reducerDispatch] = React.useReducer(root.reducer, root.state);
const getState = useGetter(state);
const [getState] = useAccessor(state);

const dispatch = React.useCallback<ContextDispatch<T, P>>(
async function contextDispatch(action) {
Expand All @@ -22,22 +21,46 @@ export function Provider<S, T extends string, P>({
[reducerDispatch],
);

const [value, enhancer] = React.useMemo<
[ContextValue<S, T, P>, ContextEnhance<S, T, P>?]
>(
function splitValueAndEnhancer() {
const { enhancer, ...unenhanced } = root;
return [{ ...unenhanced, dispatch, getState, state }, enhancer];
const unenhanced = React.useMemo<ContextValue<S, T, P>>(
function getUnenhanced() {
const { enhancer: _, ...value } = root;
return Object.assign(value, { dispatch, getState });
},
[dispatch, getState, root, state],
[dispatch, getState, root],
);

const observable = useObservable(useGetter(value));
// Get a reference that will be set once any supplied enhancer is run.
const [getEnhanced, setEnhanced] = useAccessor<ContextValue<S, T, P>>();

useOnce(function enhanceAndIntialise() {
const enhanced = enhancer?.(observable) ?? observable;
enhanced.dispatch(createAction<T, P, S>(ActionTypes.INIT as T)());
});
// This is the final value to be observed.
// The enhanced value will be given below.
const value = React.useMemo<ContextValue<S, T, P>>(
function getValue() {
return { ...unenhanced, ...getEnhanced(), state };
},
[getEnhanced, state, unenhanced],
);
// This is the observable context value that will be enhanced once.
const [observable, publish] = useObservable(value);

// This is called only on the initial render or if the enhancer changes.
// It sets the enhanced value that will supplement the unenhanced value
// on subsequent renders. It needs to be called after an observable is
// created from the value to allow the enhancer subscribe to the value.
React.useEffect(
function enhanceAndIntialise() {
const enhanced = root.enhancer?.(observable) ?? observable;
setEnhanced?.(enhanced);
enhanced.dispatch(createAction<T, P, S>(ActionTypes.INIT as T)());
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[root.enhancer],
);

// Notify only on a change to the observed value. This is scheduled after
// the enhance effect to ensure the first render notifies all subscribers.
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(publish, [observable]);

return <Context.Provider value={observable}>{children}</Context.Provider>;
}
20 changes: 20 additions & 0 deletions src/hooks/useAccessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from "react";

export function useAccessor<R>(
value?: R,
): [() => R | undefined, ((v: R) => void) | undefined] {
const ref = React.useRef(value);

const getValue = React.useCallback(function getValue() {
return ref.current;
}, []);
const setValue = React.useCallback(function setValue(value) {
ref.current = value;
}, []);

if (value) setValue(value);
return [
getValue,
React.useMemo(() => (value ? undefined : setValue), [setValue, value]),
];
}
17 changes: 0 additions & 17 deletions src/hooks/useGetter.ts

This file was deleted.

12 changes: 3 additions & 9 deletions src/hooks/useObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const asObserver = (
start: NOOP,
});

export function useObservable<V>(getter: () => V): V & Observable {
export function useObservable<V>(value: V): [V & Observable, () => void] {
type Key = Observer | OnNextFunction;

const { value: observers, add, remove } = useCollection<Key, Observer>();
Expand Down Expand Up @@ -88,25 +88,19 @@ export function useObservable<V>(getter: () => V): V & Observable {
[subscribe],
);

const value = getter();

const nextValue = React.useMemo(
function getNextValue() {
return { ...value, ...observable };
},
[observable, value],
);

const next = React.useCallback(
const observeNext = React.useCallback(
function observeNext() {
observers.forEach((observer) => observer.next(nextValue));
},
[observers, nextValue],
);

// Only call observers next for a value change
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(next, [nextValue]);

return nextValue;
return [nextValue, observeNext];
}

0 comments on commit 23d644e

Please sign in to comment.