Skip to content

Commit

Permalink
feat: add Class component support (#39)
Browse files Browse the repository at this point in the history
* feat: add Class component support

* feat: disable hooks lint rule for this case

* feat: add test suites for withStore API

* feat: add withStores API

* fix: lint

* fix: lint

* feat: higher Order Components with React and TypeScript

* chore: lint

* chore: return type

* chore: simplify Introduction, adjust chapter order and add withStore

* chore: typo

* chore: typo

* chore: format

* docs: add withStores

* feat: 将 map 的字段收敛到 store

* chore: 优化示例代码

* fix: 类型丢失

* chore: lint

* chore: 组件类的支持参数变更

* chore: 更准确的描述

* chore: typo

* chore: better description

Co-authored-by: 许文涛 <[email protected]>
  • Loading branch information
temper357 and alvinhui committed Dec 28, 2019
1 parent 9ba0669 commit db04f68
Show file tree
Hide file tree
Showing 13 changed files with 646 additions and 402 deletions.
365 changes: 214 additions & 151 deletions README.md

Large diffs are not rendered by default.

480 changes: 269 additions & 211 deletions README.zh-CN.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/todos-ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
2 changes: 1 addition & 1 deletion examples/todos-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"@ice/store": "^0.3.x",
"@ice/store": "^0.4.x",
"@ice/store-logger": "^0.1.x",
"react": "16.8.6",
"react-dom": "16.8.6"
Expand Down
90 changes: 61 additions & 29 deletions examples/todos-ts/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,66 @@
import React, { useEffect } from 'react';
import React, { Component, useEffect } from 'react';
import ReactDOM from 'react-dom';
import stores from './stores';
import {TodoStore} from './stores/todos';

function Todo() {
const {withStore} = stores;

interface CustomTodoStore extends TodoStore {
customField: string;
}

interface TodoListProps {
title: string;
store: CustomTodoStore;
}

class TodoList extends Component<TodoListProps> {
onRemove = (index) => {
const {remove} = this.props.store;
remove(index);
}

onCheck = (index) => {
const {toggle} = this.props.store;
toggle(index);
}

render() {
const {title, store} = this.props;
const {dataSource, customField} = store;
return (
<div>
<h2>{title}</h2>
<p>
{customField}
</p>
<ul>
{dataSource.map(({ name, done = false }, index) => (
<li key={index}>
<label>
<input
type="checkbox"
checked={done}
onChange={() => this.onCheck(index)}
/>
{done ? <s>{name}</s> : <span>{name}</span>}
</label>
<button type="submit" onClick={() => this.onRemove(index)}>-</button>
</li>
))}
</ul>
</div>
);
}
}

const TodoListWithStore = withStore('todos', (store: TodoStore): {store: CustomTodoStore} => {
return { store: {...store, customField: '测试的字段'} };
})(TodoList);

function TodoApp() {
const todos = stores.useStore('todos');
const { dataSource, refresh, add, remove, toggle } = todos;
const { dataSource, refresh, add } = todos;

useEffect(() => {
refresh();
Expand All @@ -15,33 +71,9 @@ function Todo() {
console.log('Newly added todo is ', todo);
}

function onRemove(index) {
remove(index);
}

function onCheck(index) {
toggle(index);
}

const noTaskView = <span>no task</span>;
const loadingView = <span>loading...</span>;
const taskView = dataSource.length ? (
<ul>
{dataSource.map(({ name, done = false }, index) => (
<li key={index}>
<label>
<input
type="checkbox"
checked={done}
onChange={() => onCheck(index)}
/>
{done ? <s>{name}</s> : <span>{name}</span>}
</label>
<button type="submit" onClick={() => onRemove(index)}>-</button>
</li>
))}
</ul>
) : (
const taskView = dataSource.length ? <TodoListWithStore title="标题" /> : (
noTaskView
);

Expand All @@ -65,4 +97,4 @@ function Todo() {
}

const rootElement = document.getElementById('ice-container');
ReactDOM.render(<Todo />, rootElement);
ReactDOM.render(<TodoApp />, rootElement);
1 change: 1 addition & 0 deletions examples/todos-ts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"experimentalDecorators": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"paths": {
Expand Down
2 changes: 1 addition & 1 deletion examples/todos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"main": "src/index.js",
"dependencies": {
"@ice/store": "^0.3.x",
"@ice/store": "^0.4.x",
"@ice/store-logger": "^0.1.x",
"react": "16.8.6",
"react-dom": "16.8.6"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/store",
"version": "0.4.0",
"version": "0.4.1",
"description": "Lightweight React state management library based on react hooks",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
41 changes: 38 additions & 3 deletions src/index.ts → src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import Store from './store';
import { Store as Wrapper, State, Middleware } from './types';
import { Store as Wrapper, State, Middleware, Optionalize } from './types';
import warning from './util/warning';

export default class Icestore {
/** Stores registered */
private stores: {[namespace: string]: any} = {};
private stores: {[namespace: string]: Store} = {};

/** Global middlewares applied to all stores */
private globalMiddlewares: Middleware[] = [];
Expand Down Expand Up @@ -51,10 +52,44 @@ export default class Icestore {
return getModel(namespace).getState<State<M[K]>>();
};

function withStore<K extends keyof M>(namespace: K, mapStoreToProps?: (store: Wrapper<M[K]>) => { store: Wrapper<M[K]>|object } ) {
type StoreProps = ReturnType<typeof mapStoreToProps>;
return <P extends StoreProps>(Component: React.ComponentClass<P>) => {
return (props: Optionalize<P, StoreProps>): React.ReactElement => {
const store: Wrapper<M[K]> = useStore(namespace);
const storeProps: StoreProps = mapStoreToProps ? mapStoreToProps(store) : {store};
return (
<Component
{...storeProps}
{...(props as P)}
/>
);
};
};
};

function withStores<K extends keyof M>(namespaces: K[], mapStoresToProps?: (stores: Models) => { stores: Models|object }) {
type StoresProps = ReturnType<typeof mapStoresToProps>;
return <P extends StoresProps>(Component: React.ComponentType<P>) => {
return (props: Optionalize<P, StoresProps>): React.ReactElement => {
const stores: Models = useStores(namespaces);
const storesProps: StoresProps = mapStoresToProps ? mapStoresToProps(stores) : {stores};
return (
<Component
{...storesProps}
{...(props as P)}
/>
);
};
};
};

return {
useStore,
useStores,
getState,
withStore,
withStores,
};
}

Expand Down Expand Up @@ -92,7 +127,7 @@ export default class Icestore {
* @param {object} model - store's model consists of state and actions
* @return {object} store instance
*/
public registerStore(namespace: string, model: {[namespace: string]: any}) {
public registerStore<T>(namespace: string, model: Wrapper<T>) {
warning('Warning: Register store via registerStore API is deprecated and about to be removed in future version. Use the registerStores API instead. Refer to https://github.com/ice-lab/icestore#getting-started for example.');
if (this.stores[namespace]) {
throw new Error(`Namespace have been used: ${namespace}.`);
Expand Down
4 changes: 2 additions & 2 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as isFunction from 'lodash.isfunction';
import * as isPromise from 'is-promise';
import isFunction from 'lodash.isfunction';
import isPromise from 'is-promise';
import { useState, useEffect } from 'react';
import compose from './util/compose';
import { ComposeFunc, Middleware } from './types';
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ interface ActionProps {
disableLoading?: boolean;
}

export type Optionalize<T extends K, K> = Omit<T, keyof K>;

export type Store<W> = {
[T in keyof W]: W[T] extends Function ? W[T] & ActionProps: W[T];
}
Expand Down
50 changes: 50 additions & 0 deletions tests/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ describe('#Icestore', () => {
let useStore;
let useStores;
let getState;
let withStore;
let withStores;
let todoStore;
let projectStore;

Expand All @@ -51,6 +53,8 @@ describe('#Icestore', () => {
useStore = stores.useStore;
useStores = stores.useStores;
getState = stores.getState;
withStore = stores.withStore;
withStores = stores.withStores;
});

test('should throw an Error when the namespace is not exist.', () => {
Expand Down Expand Up @@ -104,6 +108,52 @@ describe('#Icestore', () => {

expect(todoName.textContent).toEqual(todoStore.name);
});

test('should withStore be ok.', () => {
interface PropsType {
todo?: any;
}
@withStore('todo', (todo) => {
return {todo};
})
class App extends React.Component<PropsType> {
render() {
const {todo} = this.props;
return <div>
<span data-testid="todoName">{todo.name}</span>
</div>;
}
}
const { container } = render(<App />);
const todoName = getByTestId(container, 'todoName');

expect(todoName.textContent).toEqual(todoStore.name);
});

test('should withStores be ok.', () => {
interface PropsType {
todo?: any;
project?: any;
}
@withStores(['todo', 'project'], ({todo, project}) => {
return {todo, project};
})
class App extends React.Component<PropsType> {
render() {
const {todo, project} = this.props;
return <div>
<span data-testid="todoName">{todo.name}</span>
<span data-testid="projectName">{project.name}</span>
</div>;
}
}
const { container } = render(<App />);
const todoName = getByTestId(container, 'todoName');
const projectName = getByTestId(container, 'projectName');

expect(todoName.textContent).toEqual(todoStore.name);
expect(projectName.textContent).toEqual(projectStore.name);
});
});

describe('#applyMiddleware', () => {
Expand Down
8 changes: 5 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
"jsx": "react",
"experimentalDecorators": true,
"declaration": true,
"esModuleInterop": true,
"sourceMap": true,
"outDir": "lib"
"outDir": "lib",
"lib": ["es5", "dom"]
},
"files": [
"src/index.ts"
"src/index.tsx"
],
"exclude": [
"node_modules"
]
}
}

0 comments on commit db04f68

Please sign in to comment.