Skip to content
This repository has been archived by the owner on Jul 28, 2022. It is now read-only.

Commit

Permalink
Type safe actions and store
Browse files Browse the repository at this point in the history
fixes #15
closes #3
  • Loading branch information
imhoffd committed Feb 17, 2019
1 parent e8bcf16 commit cd44004
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 64 deletions.
135 changes: 88 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,139 @@
## Stencil Redux

A simple redux connector for Stencil-built web components inspired by react-redux.
A simple redux connector for Stencil-built web components inspired by [`react-redux`](https://github.com/reduxjs/react-redux).

## Install

```
npm install @stencil/redux
npm install redux
npm install -D @types/redux
```

## Usage

Stencil Redux uses the official redux library underneath, so much of the creation and configuration of the store, along with specifying reducers and middleware, is identical.
Stencil Redux uses the [`redux`](https://github.com/reduxjs/redux/) library underneath. Setting up the store and defining actions, reducers, selectors, etc. should be familiar to you if you've used React with Redux.

### Configure store
### Configure the Root Reducer

```typescript
// src/store/index.ts
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'; // Add-on you might want
import logger from 'redux-logger'; // Add-on you might want
import rootReducer from '../reducers/index';
// redux/reducers.ts

import { combineReducers } from 'redux';

// Import feature reducers and state interfaces.
import { TodoState, todos } from './todos/reducers';

const configureStore = (preloadedState: any) =>
createStore(rootReducer, preloadedState, applyMiddleware(thunk, logger));
// This interface represents app state by nesting feature states.
export interface RootState {
todos: TodoState;
}

export { configureStore };
// Combine feature reducers into a single root reducer
export const rootReducer = combineReducers({
todos,
});
```

### Configure reducers
### Configure the Actions

```typescript
// src/reducers/index.ts
import myReducer from './myReducer';
// redux/actions.ts

import { combineReducers } from 'redux';
import { RootState } from './reducers';

const rootReducer = (combineReducers as any)({
myReducer
});

export default rootReducer;
// Import feature action interfaces
import { TodoAction } from './todos/actions';

// Export all feature actions for easier access.
export * from './todos/actions';

// Combine feature action interfaces into a base type. Use union types to
// combine feature interfaces.
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types
export type Action = (
TodoAction
);
```

### Configure the Store

```typescript
// redux/store.ts

import { Store, applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk'; // add-on you may want
import logger from 'redux-logger'; // add-on you may want

import { RootState, rootReducer } from './reducers';

export const store: Store<RootState> = createStore(rootReducer, applyMiddleware(thunk, logger));
```

### Configure Store in Root Component

```typescript

// components/my-app/my-app.tsx

import { Store } from '@stencil/redux';
import { configureStore } from '../../store/index'; // index required due to bug

import { Action } from '../../redux/actions';
import { RootState } from '../../redux/reducers';
import { store } from '../../redux/store';

@Component({
tag: 'my-app',
styleUrl: 'my-app.scss'
})
export class MyApp {
@Prop({ context: 'store' }) store: Store;
@Prop({ context: 'store' }) store: Store<RootState, Action>;

componentWillLoad() {
this.store.setStore(configureStore({}));
this.store.setStore(store);
}
}
```

### Map state and dispatch to props

:memo: *Note*: Because the mapped props are technically changed *within* the component, `mutable: true` is required for `@Prop` definitions that utilize the store. See the [Stencil docs](https://stenciljs.com/docs/properties#prop-value-mutability) for info.

```typescript
import { Store, Action } from '@stencil/redux';
// components/my-component/my-component.tsx

import { Store, Unsubscribe } from '@stencil/redux';

import { Action, changeName } from '../../redux/actions';
import { RootState } from '../../redux/reducers';

@Component({
tag: 'my-component',
styleUrl: 'my-component.scss'
})
export class MyComponent {
@Prop({ context: 'store' }) store: Store;

@State() name: string;

changeName: Action;

private unsubscribe: () => void;

@Prop({ context: 'store' }) store: Store<Action, RootState>;
@Prop({ mutable: true }) name: string;

changeName!: typeof changeName;

unsubscribe!: Unsubscribe;

componentWillLoad() {
this.unsubscribe = this.store.mapStateToProps(this, (state) => {
const {
myReducer: { name }
} = state;
return {
name
}
this.unsubscribe = this.store.mapStateToProps(this, state => {
const { user: { name } } = state;
return { name };
});

this.store.mapDispatchToProps(this, {
changeName
});
}

doNameChange(newName: string) {
this.changeName(newName);

this.store.mapDispatchToProps(this, { changeName });
}

componentDidUnload() {
this.unsubscribe();
}

doNameChange(newName: string) {
this.changeName(newName);
}
}
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"devDependencies": {
"@stencil/core": "^0.18.0",
"@types/jest": "^24.0.5",
"@types/redux": "^3.6.0",
"husky": "^1.3.1",
"jest": "^24.1.0",
"np": "^4.0.2",
Expand Down
21 changes: 13 additions & 8 deletions src/global/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
export interface Store {
dispatch: (action: any, _: any) => any;
subscribe: (cb: (...args: any[]) => any) => any;
getState: () => any;
getStore: () => any;
setStore: (store: any) => void;
mapStateToProps: (component: any, props: any) => () => void;
mapDispatchToProps: (component: any, props: any) => void;
import { Action, AnyAction, Store as ReduxStore, Unsubscribe } from 'redux';

export interface Store<S = any, A extends Action = AnyAction> {
getState: () => S;
getStore: () => ReduxStore<S, A>;
setStore: (store: ReduxStore<S, A>) => void;
mapStateToProps: <C extends R, R>(component: C, mapper: (state: S) => R) => Unsubscribe;
mapDispatchToProps: <C extends P, P>(component: C, props: P) => void;
}

/**
* @deprecated See README.md for new usage.
*/
export type Action = (...args: any[]) => any;

export { Unsubscribe };
12 changes: 7 additions & 5 deletions src/global/store.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Store as ReduxStore } from 'redux';

import { Store } from './interfaces';

declare var Context: any;

Context.store = (() => {
let _store: Store;
Context.store = ((): Store => {
let _store: ReduxStore;

const setStore = (store: Store) => {
const setStore = (store: ReduxStore) => {
_store = store;
};

Expand All @@ -21,7 +23,7 @@ Context.store = (() => {
Object.keys(props).forEach(actionName => {
const action = props[actionName];
Object.defineProperty(component, actionName, {
get: () => (...args: any[]) => _store.dispatch(action(...args), _store),
get: () => (...args: any[]) => _store.dispatch(action(...args)),
configurable: true,
enumerable: true,
});
Expand Down Expand Up @@ -52,5 +54,5 @@ Context.store = (() => {
getState,
mapDispatchToProps,
mapStateToProps,
} as Store;
};
})();
5 changes: 1 addition & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
export {
Store,
Action,
} from './global/interfaces';
export * from './global/interfaces';

0 comments on commit cd44004

Please sign in to comment.