Skip to content

Commit

Permalink
Add Hooks api (#6)
Browse files Browse the repository at this point in the history
* Create hooks api

* Use sideEffect for hook subscription.

* Update hooks code.

* Performance improvements to the hook

* Hooks tests
  • Loading branch information
ericmackrodt committed Jul 1, 2019
1 parent 39da05e commit 07e889e
Show file tree
Hide file tree
Showing 20 changed files with 533 additions and 112 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ export {
Transformers,
Subscription,
Staat,
LegacyStaat,
StateContainerType,
IScope,
TransformersTree,
} from './types';
export * from './scope';
export const internals = {
Expand Down
8 changes: 3 additions & 5 deletions packages/core/src/state-container.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Subscription } from './types';
import { processLargeArrayAsync } from './utils';

export class StateContainer<T> {
private state: T;
Expand All @@ -10,15 +11,12 @@ export class StateContainer<T> {
}

private fireSubscriptions = () => {
const results = this.subscriptions.map(s => s());
return Promise.all(results);
processLargeArrayAsync(this.subscriptions, s => s());
};

public setState(state: T) {
this.state = state;
Promise.resolve().then(() =>
this.fireSubscriptions().then(() => this.state),
);
this.fireSubscriptions();
return this.state;
}

Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,25 @@ export function getScope<TState extends Record<string, any>, TScope>(
): TScope {
return getProperty(state, path, 0);
}

// https://stackoverflow.com/questions/10344498/best-way-to-iterate-over-an-array-without-blocking-the-ui/10344560#10344560
export function processLargeArrayAsync<T>(
array: T[],
fn: (c: T) => void,
chunk = 100,
) {
chunk = chunk || 100;
let index = 0;
const doChunk = () => {
let cnt = chunk;
while (cnt-- && index < array.length) {
fn(array[index]);
++index;
}
if (index < array.length) {
// set Timeout for async iteration
setTimeout(doChunk, 1);
}
};
doChunk();
}
12 changes: 6 additions & 6 deletions packages/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@
"dependencies": {
"staat": "1.2.0",
"staat-timetravel": "1.2.0",
"staat-react": "1.2.0",
"react": "^16.7.0",
"react-dom": "^16.7.0"
"staat-react": "1.3.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"devDependencies": {
"@types/react": "^16.7.17",
"@types/react-dom": "^16.0.11",
"@types/react": "^16.8.20",
"@types/react-dom": "^16.8.4",
"clean-webpack-plugin": "^1.0.0",
"html-webpack-plugin": "^3.2.0",
"ts-loader": "5.3.2",
"webpack": "^4.28.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
}
}
}
3 changes: 1 addition & 2 deletions packages/example/src/calculator-state-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ const subtract = calculatorScope.reducer(
},
);

const t = timeTravel(
export default timeTravel(
{
add,
subtract,
},
calculatorScope,
);
export default t;
2 changes: 2 additions & 0 deletions packages/example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import * as ReactDOM from 'react-dom';
import { Provider } from './state';
import Calculator from './calculator';
import Welcome from './welcome-component';
import { LongList } from './long-list';

ReactDOM.render(
<Provider>
<Welcome />
<Calculator />
<LongList />
</Provider>,
document.getElementById('entry'),
);
19 changes: 19 additions & 0 deletions packages/example/src/long-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { useStaat } from './state';

export const LongListItem: React.FunctionComponent = () => {
const name = useStaat(({ welcome }) => welcome.name);
return <div>Value: {name}</div>;
};

export const LongList: React.FunctionComponent = () => {
return (
<div>
{Array(5000)
.fill(undefined)
.map((_, index) => (
<LongListItem key={index} />
))}
</div>
);
};
4 changes: 3 additions & 1 deletion packages/example/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ const initialState = {

export const appState = staat(initialState);

export const { connect, Provider } = reactStaat(appState);
export const { connect, Provider, useReducers, useStaat } = reactStaat(
appState,
);
42 changes: 10 additions & 32 deletions packages/example/src/welcome-component.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,21 @@
import * as React from 'react';

import { connect, appState } from './state';
import * as welcome from './welcome-state-definition';
import { useReducers, useStaat } from './state';
import * as welcomeDefinition from './welcome-state-definition';

const Welcome: React.StatelessComponent<WelcomeProps> = props => {
const Welcome: React.FunctionComponent = () => {
const name = useStaat(({ welcome }) => welcome.name);
const { setName } = useReducers({
setName: welcomeDefinition.setName,
});
return (
<div>
<h1>Welcome, {props.name}</h1>
<input type='text' onChange={evt => props.setName(evt.target.value)} />
<h1>Welcome, {name}</h1>
<input type='text' onChange={evt => setName(evt.target.value)} />
{/* <button onClick={() => props.undo()}>Undo</button>
<button onClick={() => props.redo()}>Redo</button> */}
</div>
);
};
type StateProps = {
name?: string;
};

type TrasformerProps = {
setName: (name: string) => void;
// undo: typeof welcomeState.undo;
// redo: typeof welcomeState.redo;
};

type OwnProps = {};

type WelcomeProps = StateProps & TrasformerProps & OwnProps;

export default connect<OwnProps, StateProps, TrasformerProps>(
state => {
return {
name: state.welcome.name,
};
},
() => {
return {
setName: (name: string) => appState.reduce(welcome.setName, name),
// undo: welcome.undo.bind(welcomeState),
// redo: welcomeState.redo.bind(welcomeState)
};
},
)(Welcome);
export default Welcome;
12 changes: 7 additions & 5 deletions packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "staat-react",
"version": "1.2.0",
"version": "1.3.0",
"description": "",
"main": "build/index.js",
"typings": "build/index.d.ts",
Expand All @@ -13,19 +13,21 @@
"license": "ISC",
"repository": "https://github.com/ericmackrodt/staat",
"devDependencies": {
"@types/react": "^16.7.17",
"@types/react": "^16.8.20",
"@babel/core": "7.3.3",
"@testing-library/react": "^8.0.4",
"@testing-library/react-hooks": "^1.1.0",
"babel-loader": "8.0.5",
"cross-env": "^5.2.0",
"jest-dom": "^3.0.0",
"metro-react-native-babel-preset": "0.51.1",
"react-testing-library": "^5.4.4",
"react-test-renderer": ">=16.8.3",
"ts-loader": "5.3.2",
"webpack": "^4.28.1",
"webpack-cli": "^3.1.2"
},
"peerDependencies": {
"react": ">=16.5.0",
"react": ">=16.8.3",
"staat": "1.2.0"
}
}
}
152 changes: 152 additions & 0 deletions packages/react/src/__tests__/hooks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { reactStaat } from '../react';
import staat from 'staat';
import { renderHook, act } from '@testing-library/react-hooks';
import { wait } from '@testing-library/react';

type TestState = {
count: number;
other: string;
};

const state: TestState = {
count: 0,
other: 'tst',
};

it('builds react-staat object', () => {
const staatState = staat({ ...state });
const sut = reactStaat(staatState);
expect(typeof sut.Provider).toBe('function');
expect(typeof sut.connect).toBe('function');
expect(typeof sut.useStaat).toBe('function');
expect(typeof sut.useReducers).toBe('function');
});

describe('Hooks', () => {
describe('useStaat', () => {
it('updates the value', async () => {
const staatState = staat({ ...state });
const { useStaat } = reactStaat(staatState);

const { result } = renderHook(() => useStaat(({ count }) => count));

act(() => {
staatState.reduce(s => ({ ...s, count: 1000 }));
});

await wait(() => expect(result.current).toBe(1000));
});
});

it('unsubscribes from staat', async () => {
const staatState = staat({ ...state });
const { useStaat } = reactStaat(staatState);
staatState.unsubscribe = jest.fn();
const { unmount } = renderHook(() => useStaat(({ count }) => count));
unmount();
expect(staatState.unsubscribe).toHaveBeenCalled();
});

it('updates if number of members is different', async () => {
const staatState = staat({ ...state });
const { useStaat } = reactStaat(staatState);

const { result } = renderHook(() => useStaat(({ count }) => count));

act(() => {
staatState.reduce(sts => {
return { ...sts, count: 200, another: '1' };
});
});

await wait(() => expect(result.current).toBe(200));
});

it('does not update the component if state is same object', async () => {
const original = { ...state };
const staatState = staat(original);
const { useStaat } = reactStaat(staatState);

const { result } = renderHook(() => useStaat(sts => sts));

act(() => {
staatState.reduce(sts => {
sts.count = 200;
return sts;
});
});
await wait(() => expect(result.current).toBe(original), { timeout: 100 });
expect(original.count).toBe(200);
});

it('does not update the component if state members are equal', async () => {
const original = { ...state };
const staatState = staat(original);
const { useStaat } = reactStaat(staatState);

const { result } = renderHook(() => useStaat(sts => sts));

act(() => {
staatState.reduce(sts => {
return { ...sts };
});
});
await wait(() => expect(result.current).toBe(original), { timeout: 100 });
expect(result.current).toBe(original);
});

it('updates when same number of properties but different name', async () => {
const original: { a: string; b?: number; c?: number } = { a: '1', b: 2 };
const staatState = staat(original);
const { useStaat } = reactStaat(staatState);

const { result } = renderHook(() => useStaat(sts => sts));

act(() => {
staatState.reduce(() => {
return { a: '1', c: 2 };
});
});
await wait(() => expect(result.current).toEqual({ a: '1', c: 2 }), {
timeout: 100,
});
});

describe('useReducers', () => {
it('changes the state without arguments', () => {
const staatState = staat({ ...state });
const { useReducers } = reactStaat(staatState);
const newValue = 10;

const { result } = renderHook(() =>
useReducers({
testReducer: (sts: TestState) => ({ ...sts, count: newValue }),
}),
);
act(() => {
result.current.testReducer();
});

expect(staatState.currentState.count).toBe(10);
});

it('changes the state with arguments', () => {
const staatState = staat({ ...state });
const { useReducers } = reactStaat(staatState);

const { result } = renderHook(() =>
useReducers({
testReducer: (sts: TestState, value: number) => ({
...sts,
count: value,
}),
}),
);
act(() => {
result.current.testReducer(300);
});

expect(staatState.currentState.count).toBe(300);
});
});
});
Loading

0 comments on commit 07e889e

Please sign in to comment.