Skip to content

Commit

Permalink
Merge 9211973 into e9a5f61
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrius committed Aug 8, 2019
2 parents e9a5f61 + 9211973 commit 2ff0f81
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 65 deletions.
14 changes: 11 additions & 3 deletions README.md
Expand Up @@ -118,14 +118,22 @@ function Counter() {
}
```

##### `<Provider>`
##### `<Container>`

The final piece that Outstated has is `<Provider>` component.
It has two roles:

1. It initializes global instances of given stores (this is required because React expects the number of hooks to be consistent across re-renders)
1. It initializes global instances of given store (this is required because React expects the number of hooks to be consistent across re-renders)
2. It uses context to pass initialized instances of given stores to all the components down the tree


##### `<Provider>`

The final piece that Outstated has is `<Provider>` component.

1. It initializes different contexts for each store
2. It initializes `<Container>` components according to contexts initializes before (this is required not to make unnecessary re-render to component which relies on Store A after Store B was changed)
3. It nests `<Container>` components' list and returns the tree

```jsx
render(
<Provider stores={[counterStore]}>
Expand Down
85 changes: 53 additions & 32 deletions src/index.js
@@ -1,40 +1,61 @@
import React, {createContext, useContext} from 'react';

// Create context for global store assignment
const StateContext = createContext();

export const Provider = ({stores, children}) => {
// map that stores initialized versions of all user store hooks
const storesMap = new Map();
// complain if no instances provided for initialization
if (!stores || !stores.length) {
throw new Error('You must provide stores list to a <Provider> for initialization!');
}
// initialize store hooks
// this is required because react expects the same number
// of hooks to be called on each render
// so if we run init in useStore hook - it'll break on re-render
stores.forEach(store => {
storesMap.set(store, store());
});
// return provider with stores map
return <StateContext.Provider value={storesMap}>{children}</StateContext.Provider>;
import React, { createContext, useContext } from 'react';

// Create context map for global store assignment
const ContextMap = new Map();

const Container = ({ store, children }) => {

// initialize store hooks
// this is required because react expects the same number
// of hooks to be called on each render
// so if we run init in useStore hook - it'll break on re-render
// return provider with stores map
const storesMap = new Map([[store, store()]]);

// get context for specific store
const Context = ContextMap.get(store);
return (
<Context.Provider value={storesMap}>{children}</Context.Provider>
);
};

export const Provider = ({ stores, children }) => {
// complain if no instances provided for initialization
if (!stores || !stores.length) {
throw new Error(
'You must provide stores list to a <Provider> for initialization!'
);
}

// create providers for each store
let providersLayout;

stores.forEach(store => {
let context = ContextMap.get(store);
if (!context) {
context = createContext();
ContextMap.set(store, context);
}
providersLayout = (<Container store={store}>{providersLayout || children}</Container>);
});
return providersLayout;
};

export function useStore(storeInit) {
const map = useContext(StateContext);
// use store specific context
const map = useContext(ContextMap.get(storeInit));

// complain if no map is given
if (!map) {
throw new Error('You must wrap your components with a <Provider>!');
}
// complain if no map is given
if (!map) {
throw new Error('You must wrap your components with a <Provider>!');
}

const instance = map.get(storeInit);
const instance = map.get(storeInit);

// complain if instance wasn't initialized
if (!instance) {
throw new Error('Provided store instance did not initialized correctly!');
}
// complain if instance wasn't initialized
if (!instance) {
throw new Error('Provided store instance did not initialized correctly!');
}

return instance;
return instance;
}
124 changes: 94 additions & 30 deletions tests/main.test.js
@@ -1,73 +1,137 @@
/* eslint-env jest */
/* global spyOn */
import React, {useState} from 'react';
import {cleanup, fireEvent, render} from 'react-testing-library';
import {renderHook, act} from 'react-hooks-testing-library';
import {Provider, useStore} from '../src';
import React, { useState } from 'react';
import { cleanup, fireEvent, render } from 'react-testing-library';
import { renderHook, act } from 'react-hooks-testing-library';
import { Provider, useStore } from '../src';

const counterStore = () => {
const oddsStore = () => {
const [count, setCount] = useState(1);

const increment = (amount = 2) => setCount(count + amount);
const decrement = (amount = 2) => setCount(count - amount);

return { count, setCount, increment, decrement };
};

const evensStore = () => {
const [count, setCount] = useState(0);

const increment = (amount = 1) => setCount(count + amount);
const decrement = (amount = 1) => setCount(count - amount);
const increment = (amount = 2) => setCount(count + amount);
const decrement = (amount = 2) => setCount(count - amount);

return { count, setCount, increment, decrement };
}

const OddCounter = () => {
const { count, setCount, increment, decrement } = useStore(oddsStore);

const updateState = () => setCount(101);

console.log('Render');

return {count, setCount, increment, decrement};
return (
<div>
<span>Count Odd: <span data-testid='oddResult'>{count}</span></span>
<button data-testid='odd-' onClick={() => decrement()}>-</button>
<button data-testid='odd+' onClick={() => increment()}>+</button>
<button data-testid='oddSet' onClick={updateState}>set</button>
</div>
);
};

const Counter = () => {
const {count, setCount, increment, decrement} = useStore(counterStore);
const EvenCounter = () => {
const { count, setCount, increment, decrement } = useStore(evensStore);

const updateState = () => setCount(100);

console.log('Render');

return (
<div>
<span>Count: {count}</span>
<button onClick={() => decrement()}>-</button>
<button onClick={() => increment()}>+</button>
<button onClick={updateState}>set</button>
<span>Count Even: <span data-testid='evenResult'>{count}</span></span>
<button data-testid='even-' onClick={() => decrement()}>Even -</button>
<button data-testid='even+' onClick={() => increment()}>Even +</button>
<button data-testid='evenSet' onClick={updateState}>set</button>
</div>
);
};

const brokenStore = () => {};
const Counter = () => (
<>
<OddCounter />
<EvenCounter />
</>
)

const brokenStore = () => { };

const BrokenCounter = () => {
const {state} = useStore(brokenStore);
const { state } = useStore(brokenStore);
return <div>{state}</div>;
};

afterEach(cleanup);

test('should incresase/decrease state counter in hook', () => {
test('should increase/decrease state counter in hook', () => {
let count, setCount;
renderHook(() => ({count, setCount} = counterStore()));
renderHook(() => ({ count, setCount } = oddsStore()));

expect(count).toBe(0);
expect(count).toBe(1);

act(() => {
setCount(1);
});
expect(count).toBe(1);
});

test('should incresase/decrease state counter in container', () => {
const {getByText} = render(
<Provider stores={[counterStore]}>
test('should increase/decrease state counter in hook', () => {
let count, setCount;
renderHook(() => ({ count, setCount } = evensStore()));

expect(count).toBe(0);

act(() => {
setCount(2);
});
expect(count).toBe(2);
});

test('should increase/decrease state counter in container', () => {
const { getByTestId } = render(
<Provider stores={[oddsStore, evensStore]}>
<Counter />
</Provider>
);

expect(getByText(/^Count:/).textContent).toBe('Count: 0');
spyOn(console, 'log');

expect(getByTestId('oddResult').textContent).toBe('1');
expect(getByTestId('evenResult').textContent).toBe('0');

fireEvent.click(getByTestId('odd+'));
fireEvent.click(getByTestId('odd+'));
expect(getByTestId('oddResult').textContent).toBe('5');

fireEvent.click(getByTestId('even+'));
fireEvent.click(getByTestId('even+'));
expect(getByTestId('evenResult').textContent).toBe('4');

fireEvent.click(getByTestId('odd-'));
fireEvent.click(getByTestId('odd-'));
expect(getByTestId('oddResult').textContent).toBe('1');

fireEvent.click(getByTestId('even-'));
fireEvent.click(getByTestId('even-'));
expect(getByTestId('evenResult').textContent).toBe('0');

fireEvent.click(getByText('+'));
fireEvent.click(getByText('+'));
expect(getByText(/^Count:/).textContent).toBe('Count: 2');
fireEvent.click(getByTestId('oddSet'));
expect(getByTestId('oddResult').textContent).toBe('101');

fireEvent.click(getByText('-'));
expect(getByText(/^Count:/).textContent).toBe('Count: 1');
fireEvent.click(getByTestId('evenSet'));
expect(getByTestId('evenResult').textContent).toBe('100');

fireEvent.click(getByText('set'));
expect(getByText(/^Count:/).textContent).toBe('Count: 100');
expect(console.log).toHaveBeenCalledTimes(10);
});

test('should throw error when no provider is given', () => {
Expand Down

0 comments on commit 2ff0f81

Please sign in to comment.