Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Upgrade to TypeScript 4 and remove deprecated API #118

Merged
merged 2 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ yarn add constate

## API

### `constate(useValue[, ...splitValues])`
### `constate(useValue[, ...selectors])`

Constate exports a single factory method. As parameters, it receives [`useValue`](#usevalue) and multiple optional [`splitValue`](#splitvalues) functions. It returns a tuple of `[Provider, ...contextHooks]`.
Constate exports a single factory method. As parameters, it receives [`useValue`](#usevalue) and optional [`selector`](#selectors) functions. It returns a tuple of `[Provider, ...hooks]`.

#### `useValue`

Expand Down Expand Up @@ -181,11 +181,11 @@ function Count() {
}
```

#### `splitValues`
#### `selectors`

Optionally, you can pass in one or more functions to split the custom hook value into multiple React Contexts. This is useful so you can avoid unnecessary re-renders on components that only depend on a part of the state.

A `splitValue` function receives the value returned by [`useValue`](#usevalue) and returns the value that will be held by that particular Context.
A `selector` function receives the value returned by [`useValue`](#usevalue) and returns the value that will be held by that particular Context.

```jsx
import React, { useState, useCallback } from "react";
Expand Down
154 changes: 56 additions & 98 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
import * as React from "react";
import { SplitValueFunction, ContextHookReturn } from "./types";

// constate(useCounter, value => value.count)
// ^^^^^^^^^^^^^^^^^^^^
type Selector<Value> = (value: Value) => any;

// const [Provider, useCount, useIncrement] = constate(...)
// ^^^^^^^^^^^^^^^^^^^^^^
type SelectorHooks<Selectors> = {
[K in keyof Selectors]: () => Selectors[K] extends (...args: any) => infer R
? R
: never;
};

// const [Provider, useCounterContext] = constate(...)
// or ^^^^^^^^^^^^^^^^^
// const [Provider, useCount, useIncrement] = constate(...)
// ^^^^^^^^^^^^^^^^^^^^^^
type Hooks<
Value,
Selectors extends Selector<Value>[]
> = Selectors["length"] extends 0 ? [() => Value] : SelectorHooks<Selectors>;

// const [Provider, useContextValue] = constate(useValue)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
type ConstateTuple<Props, Value, Selectors extends Selector<Value>[]> = [
React.FC<Props>,
...Hooks<Value, Selectors>
];

const isDev = process.env.NODE_ENV !== "production";

Expand All @@ -10,118 +37,49 @@ function createUseContext(context: React.Context<any>): any {
const value = React.useContext(context);
if (isDev && value === NO_PROVIDER) {
// eslint-disable-next-line no-console
console.warn("[constate] Component not wrapped within a Provider.");
console.warn("Component must be wrapped within Provider.");
}
return value;
};
}

function warnAboutObjectUsage() {
if (isDev) {
// eslint-disable-next-line no-console
console.warn(
"[constate] Getting { Context, Provider } from constate is deprecated. " +
"Please, use the tuple format instead. " +
"See instructions on https://github.com/diegohaz/constate/pull/101"
);
}
}

function constate<P, V, S extends Array<SplitValueFunction<V>>>(
useValue: (props: P) => V,
...splitValues: S
): ContextHookReturn<P, V, S> {
const Context = React.createContext(NO_PROVIDER as V);

const Provider: React.FunctionComponent<P> = (props) => {
const value = useValue(props);
return <Context.Provider value={value}>{props.children}</Context.Provider>;
function constate<Props, Value, Selectors extends Selector<Value>[]>(
useValue: (props: Props) => Value,
...selectors: Selectors
): ConstateTuple<Props, Value, Selectors> {
const contexts = [] as React.Context<any>[];
const hooks = ([] as unknown) as Hooks<Value, Selectors>;

const createContext = () => {
const context = React.createContext(NO_PROVIDER);
contexts.push(context);
hooks.push(createUseContext(context));
};

if (isDev && useValue.name) {
Context.displayName = `${useValue.name}.Context`;
Provider.displayName = `${useValue.name}.Provider`;
if (selectors.length) {
selectors.forEach(createContext);
} else {
createContext();
}

// const useCounterContext = constate(...)
const useContext: any = () => {
if (isDev) {
// eslint-disable-next-line no-console
console.warn(
"[constate] Using the return value of constate as a hook is deprecated. " +
"Please, use the tuple format instead. " +
"See instructions on https://github.com/diegohaz/constate/pull/101"
const Provider: React.FC<Props> = ({ children, ...props }) => {
const value = useValue(props as Props);
let element = children as React.ReactElement;
for (let i = 0; i < contexts.length; i += 1) {
const context = contexts[i];
const selector = selectors[i] || ((v) => v);
element = (
<context.Provider value={selector(value)}>{element}</context.Provider>
);
}
return createUseContext(Context)();
return element;
};

// const { Context, Provider } = constate(...)
Object.defineProperties(useContext, {
Context: {
get() {
warnAboutObjectUsage();
return Context;
},
},
Provider: {
get() {
warnAboutObjectUsage();
return Provider;
},
},
});

const tuple = [] as any[];

if (!splitValues.length) {
// const [Provider, useCounterContext] = constate(...);
tuple.push(Provider, createUseContext(Context));
} else {
const contexts = [] as Array<React.Context<any>>;

const SplitProvider: React.FunctionComponent<P> = (props) => {
const value = useValue(props);
let children = props.children as React.ReactElement;

for (let i = 0; i < contexts.length; i += 1) {
const context = contexts[i];
// splitValue may be a hook, but it won't change between re-renders
const splitValue = splitValues[i];
children = (
<context.Provider value={splitValue(value)}>
{children}
</context.Provider>
);
}

return children;
};

if (isDev && useValue.name) {
SplitProvider.displayName = `${useValue.name}.Provider`;
}

// const [Provider, useCount, useIncrement] = constate(...);
tuple.push(SplitProvider);

for (let i = 0; i < splitValues.length; i += 1) {
const context = React.createContext(NO_PROVIDER);
contexts.push(context);
tuple.push(createUseContext(context));
}
}

for (let i = 0; i < tuple.length; i += 1) {
useContext[i] = tuple[i];
}

if (typeof Symbol === "function" && Symbol.iterator) {
useContext[Symbol.iterator] = /* istanbul ignore next */ () =>
tuple[Symbol.iterator]();
if (isDev && useValue.name) {
Provider.displayName = `${useValue.name}.Provider`;
}

return useContext;
return [Provider, ...hooks];
}

export default constate;
65 changes: 0 additions & 65 deletions src/types.ts

This file was deleted.