diff --git a/README.md b/README.md index 4089f6e..1dacd66 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,15 @@ function AwaitedComponent() { - Simple APIs and you just read all of them - No dependency but React 16.8 or higher and [tslib](https://github.com/Microsoft/tslib) for TypeScript projects - Strongly typed with TypeScript. As long as store types are specified, other types can be inferred so you don't have to `as` or `any` anymore! +- Nested providers. Inner provided stores will override outer stores. - (WIP) Basic SSR utilities support # Common Pitfalls -- To avoid unnecessary re-renders, `StoreProvider` is designed to only re-render when the current and previous `stores` are different. For two `Store[]` *a* and *b*, `a differs from b` is equivalent to `a.length === b.length && for i from 0 to a.length-1, a[i] === b[i]`. +- Under the hood, the provided stores will be stored in a `Map` with store's constructor as the key and store instance itself as the value. To avoid unnecessary re-renders, `StoreProvider` is designed to cache inner context and only reassign context only if the provided maps are different. Two maps are different if they have different length, or contains different stores. - Once the promise returned by `setState` resolves, it is guaranteed for components that use render props and HOC that have updated, but these using `useStore` hook will not have the guarantee. I am working on it. + ## Roadmap - [x] Store and `useStore` hook diff --git a/package.json b/package.json index 9a3f384..94d73c2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "clean:dist": "rimraf dist", "watch": "rollup -cw", "start": "rollup -cw", - "lint": "tslint 'src/**/*.{ts,tsx}' --fix", + "lint": "tslint '{src,tests}/**/*.{ts,tsx}' --fix", "test": "jest", "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls", "release": "yarn run build && standard-version" @@ -30,13 +30,13 @@ "tslib": "^1.9.3" }, "peerDependencies": { - "react": ">=16.8.0" + "react": ">=16.8.1" }, "devDependencies": { - "react": ">=16.8.0", + "react": ">=16.8.1", "@types/enzyme": "^3.1.17", - "@types/jest": "^24.0.0", - "@types/react": ">=16.8.0", + "@types/jest": "^24.0.1", + "@types/react": ">=16.8.2", "@types/react-test-renderer": "^16.8.0", "coveralls": "^3.0.2", "cross-env": "^5.2.0", diff --git a/src/StoreProvider.tsx b/src/StoreProvider.tsx index 10d48a7..36a4861 100644 --- a/src/StoreProvider.tsx +++ b/src/StoreProvider.tsx @@ -10,48 +10,62 @@ interface Props { children: React.ReactNode; } -// interface State { -// map: ISimstateContext; -// } +interface State { + map: ISimstateContext; +} -function arrayEquals(array1: T[], array2: T[]) { - const len = array1.length; - if (len !== array2.length) { +function contextEqual(a: ISimstateContext, b: ISimstateContext) { + if (a.size !== b.size) { return false; } - - for (let i = 0; i < len; i++) { - if (array1[i] !== array2[i]) { + for (const [key, val] of a) { + if (b.get(key) !== val) { return false; } } return true; - } -function constructMap(stores: Store[]) { - const map: ISimstateContext = new Map(); - stores.forEach((store) => { - map.set(store.constructor as StoreType, store); - }); - return map; +interface InnerProps { + currentMap: ISimstateContext; } -export default class StoreProvider extends React.Component { +class StoreProvider extends React.Component { - shouldComponentUpdate(prevProps: Props) { - return !arrayEquals(prevProps.stores, this.props.stores); + componentDidUpdate() { + if (!contextEqual(this.props.currentMap, this.state.map)) { + this.setState({ map: this.props.currentMap }); + } } - render() { - - const map = constructMap(this.props.stores); + state = { map: this.props.currentMap }; + render() { return ( - + {this.props.children} ); } } + +function constructMap(prev: ISimstateContext | undefined, stores: Store[]): ISimstateContext { + const map: ISimstateContext = new Map(prev!); // new Map(undefined) will work + + for (const store of stores) { + map.set(store.constructor as StoreType, store); + } + + return map; +} + +export default (props: Props) => ( + + {(map) => ( + + {props.children} + + )} + +); diff --git a/tests/provider.spec.tsx b/tests/provider.spec.tsx index 790cea8..5f1962f 100644 --- a/tests/provider.spec.tsx +++ b/tests/provider.spec.tsx @@ -43,25 +43,27 @@ describe("Provider", () => { expect(wrapper.find("span").text()).toEqual("43"); - wrapper.setProps({ stores: [new TestStore(43), new AnotherStore() ] }); + wrapper.setProps({ stores: [new TestStore(43), new AnotherStore()] }); expect(wrapper.find("span").text()).toEqual("43"); }); it("should not re-render if the input store hasn't changed", () => { - function Child() { - return ( - - {(map) => { - return ( - {map!.get(TestStore)!.state.value} - ); - } - } - - ); + class Child extends React.PureComponent { + render() { + return ( + + {(map) => { + return ( + {map!.get(TestStore)!.state.value} + ); + }} + + ); + } } + class Component extends React.Component<{ store: Store }> { render() { return ( @@ -85,4 +87,60 @@ describe("Provider", () => { }); + it("should support nested providers", () => { + class AnotherStore extends Store<{ text: string }> { + constructor(text: string) { + super(); + this.state = { text }; + } + } + + function Child() { + return ( + + {(map) => ( +
+ {map!.has(TestStore) ? map!.get(TestStore)!.state.value : "no"} + {map!.has(AnotherStore) ? map!.get(AnotherStore)!.state.text : "no"} +
+ )} +
+ ); + } + + const test = (Parent: React.ComponentType, testStore: string, anotherStore: string) => { + const wrapper = mount(); + + expect(wrapper.find("#TestStore").text()).toEqual(testStore); + expect(wrapper.find("#AnotherStore").text()).toEqual(anotherStore); + }; + + const Parent1 = () => ( + + + + ); + + test(Parent1, "42", "no"); + + const Parent2 = () => ( + + + + ); + + test(Parent2, "42", "hahaha"); + + const Parent3 = () => ( + + + + + + ); + + test(Parent3, "no", "inner"); + + }); + }); diff --git a/tsconfig.json b/tsconfig.json index 0245932..50d5080 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,9 +10,10 @@ "noUnusedLocals": true, "strictNullChecks": true, "esModuleInterop": true, + "downlevelIteration": true, "sourceMap": true, "declaration": true, - "importHelpers": true + "importHelpers": true, }, "include": [ "src/**/*.tsx" diff --git a/tslint.json b/tslint.json index 22aa7de..1e532e6 100644 --- a/tslint.json +++ b/tslint.json @@ -14,7 +14,7 @@ "member-ordering": false, "no-empty": false, "no-console": false, - "max-classes-per-file": [ true, 3], + "max-classes-per-file": false, "jsx-no-multiline-js": false, "jsx-alignment": false, "jsx-no-lambda": false,