Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix flow typing | remove createSender API #7

Merged
merged 5 commits into from
Jul 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 24 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,56 +368,33 @@ If you‘re having troubles understanding this example, I recommend the fantasti

_[Flow][] is a static type checker for JavaScript. This section is only relevant for you if you‘re using Flow in your application._

ReComponent comes with first class Flow support built in. By default, a ReComponent will behave like a regular Component and will require props and state to be typed:
ReComponent comes with first class Flow support built in.
When extending `ReComponent`, in addition to the `Props` and `State` types required by regular `React.Component`
we need to specify the third generic parameter which should be a union of all actions used by the component.
This ensures type-safety everywhere in the code of the component where the actions are used and
even allows [exhaustiveness testing] to verify that every action is indeed handled.

```js
import * as React from "react";
import { ReComponent, Update } from "react-recomponent";

type Props = {};
type State = { count: number };
type State = { count: number, value: string };
type Action = {| type: "CLICK" |} | {| type: "UPDATE_VALUE", payload: string |};

class UntypedActionTypes extends ReComponent<Props, State> {
handleClick = this.createSender("CLICK");
state = { count: 0 };

static reducer(action, state) {
switch (action.type) {
case "CLICK":
return Update({ count: state.count + 1 });
default:
return NoUpdate();
}
}

render() {
return (
<button onClick={this.handleClick}>
You’ve clicked this {this.state.count} times(s)
</button>
);
}
}
```
class TypedActions extends ReComponent<Props, State, Action> {
// NOTE: we use `this.send` API because it ensures type-safety for action's `payload`
handleClick = () => this.send({ type: "CLICK" });
handleUpdateValue = (newValue: string) => this.send({ type: "UPDATE_VALUE", payload: newValue });

Without specifying our action types any further, we will allow all `string` values. It is, however, recommended that we type all action types using a union of string literals. This will further tighten the type checks and will even allow [exhaustiveness testing] to verify that every action is indeed handled.

```js
import * as React from "react";
import { ReComponent, Update } from "react-recomponent";

type Props = {};
type State = { count: number };
type ActionTypes = "CLICK";

class TypedActionTypes extends ReComponent<Props, State, ActionTypes> {
handleClick = this.createSender("CLICK");
state = { count: 0 };

static reducer(action, state) {
switch (action.type) {
case "CLICK":
return Update({ count: state.count + 1 });
case "UPDATE_VALUE":
return Update({ value: action.payload });
default: {
return NoUpdate();
}
Expand All @@ -426,9 +403,12 @@ class TypedActionTypes extends ReComponent<Props, State, ActionTypes> {

render() {
return (
<button onClick={this.handleClick}>
You’ve clicked this {this.state.count} times(s)
</button>
<React.Fragment>
<button onClick={this.handleClick}>
You’ve clicked this {this.state.count} times(s)
</button>
<Input onValueChange={this.handleValueUpdate} />
<React.Fragment/>
);
}
}
Expand All @@ -438,7 +418,11 @@ Check out the [type definition tests](https://github.com/philipp-spiess/react-re

**Known Limitations With Flow:**

- While it is possible to exhaustively type check the reducer, Flow will still require every branch to return an effect. This is why the above examples returns `NoUpdate()` even though the branch can never be reached.
- `this.send` API for sending actions is preferred over `this.createSender`. This is because `this.createSender`
effectively types the payload as `any` (limitation we can't overcome for now), whereas `this.send` provides full type-safety
for actions
- While it is possible to exhaustively type check the reducer, Flow will still require every branch to return an effect.
This is why the above examples returns `NoUpdate()` even though the branch can never be reached.

## API Reference

Expand Down
2 changes: 1 addition & 1 deletion __tests__/Context-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("ReComponent", () => {
class Container extends ReComponent {
constructor() {
super();
this.handleClick = this.createSender("CLICK");
this.handleClick = () => this.send({ type: "CLICK" });
this.state = { count: 0 };
}

Expand Down
13 changes: 3 additions & 10 deletions __tests__/ReComponent-test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import React from "react";
import ReactDOM from "react-dom";
import { Record } from "immutable";

import {
ReComponent,
NoUpdate,
Update,
SideEffects,
UpdateWithSideEffects
} from "../src";
import { ReComponent, NoUpdate, Update } from "../src";

import { click, withConsoleMock } from "./helpers";

Expand Down Expand Up @@ -95,7 +88,7 @@ describe("ReComponent", () => {
class Example extends ReComponent {
constructor() {
super();
click = this.createSender("CLICK");
click = () => this.send({ type: "CLICK" });
}

static reducer(action, state) {
Expand Down Expand Up @@ -123,7 +116,7 @@ describe("ReComponent", () => {
class ClassPropertyReducer extends ReComponent {
constructor() {
super();
click = this.createSender("CLICK");
click = () => this.send({ type: "CLICK" });
}

reducer(action, state) {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/ReComponentImmutable-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe("ReComponentImmutable", () => {
class Example extends ReComponent {
constructor() {
super();
this.handleClick = this.createSender("CLICK");
this.handleClick = () => this.send({ type: "CLICK" });
}

initialImmutableState(props) {
Expand Down
13 changes: 7 additions & 6 deletions __tests__/UpdateTypes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ describe("UpdateTypes", () => {
class ReducerReturns extends ReComponent {
constructor() {
super();
noUpdate = this.createSender("NO_UPDATE");
update = this.createSender("UPDATE");
sideEffects = this.createSender("SIDE_EFFECTS");
updateWithSideEffects = this.createSender("UPDATE_WITH_SIDE_EFFECTS");
invalid = this.createSender("INVALID");
unhandled = this.createSender("UNHANDLED");
noUpdate = () => this.send({ type: "NO_UPDATE" });
update = () => this.send({ type: "UPDATE" });
sideEffects = () => this.send({ type: "SIDE_EFFECTS" });
updateWithSideEffects = () =>
this.send({ type: "UPDATE_WITH_SIDE_EFFECTS" });
invalid = () => this.send({ type: "INVALID" });
unhandled = () => this.send({ type: "UNHANDLED" });
this.state = { count: 0 };
}

Expand Down
15 changes: 6 additions & 9 deletions type-definitions/ReComponent.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import * as React from "react";

declare opaque type UpdateType;

export type Sender<AT, A = mixed> = (a: A) => { action: AT, payload: A };

declare export function NoUpdate(): {| type: UpdateType |};
declare export function Update<S>(state: S): {| type: UpdateType, state: S |};
declare export function SideEffects<T>(
Expand All @@ -22,13 +20,13 @@ declare export function UpdateWithSideEffects<S, T>(

declare export class ReComponent<
Props,
State = void,
ActionType = string
State,
Action: { +type: string },
> extends React.Component<Props, State> {
initialState(props: Props): State;

static reducer(
action: { type: ActionType },
action: Action,
state: State
):
| {| type: UpdateType |}
Expand All @@ -40,13 +38,12 @@ declare export class ReComponent<
sideEffects: ($Subtype<this>) => mixed
|};

send(action: { type: ActionType, payload?: mixed }): void;

createSender<AT: ActionType>(actionType: ActionType): Sender<AT>;
send(action: Action): void;
createSender<A: Action>(actionType: $ElementType<A, 'type'>): (mixed) => A;

// This type is only used when initialState returns an Immutable.js data type.
// Consider this API unstable.
+immutableState: State;
}

declare export class RePureComponent<P, S, A> extends ReComponent<P, S, A> {}
declare export class RePureComponent<P, S, A: { +type: string }> extends ReComponent<P, S, A> {}
Loading