Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Enhance/compose paths #11

Merged
merged 7 commits into from
Jul 17, 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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "restful-react",
"description": "A declarative client from RESTful React Apps",
"version": "4.0.0-2",
"version": "4.0.0-3",
"main": "dist/index.js",
"license": "MIT",
"files": [
Expand Down Expand Up @@ -49,7 +49,7 @@
"git add"
],
"*.(tsx|ts)": [
"tslint --fix",
"tslint --fix --project .",
"prettier --write",
"git add"
]
Expand Down
2 changes: 1 addition & 1 deletion src/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const { Provider, Consumer: RestfulReactConsumer } = React.createContext<Restful
});

export default class RestfulReactProvider<T> extends React.Component<RestfulReactProviderProps<T>> {
render() {
public render() {
const { children, ...value } = this.props;
return <Provider value={value}>{children}</Provider>;
}
Expand Down
54 changes: 33 additions & 21 deletions src/Get.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import RestfulProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context";

/**
* A function that resolves returned data from
Expand Down Expand Up @@ -41,7 +41,7 @@ export interface Meta {
/**
* Props for the <Get /> component.
*/
export interface GetComponentProps<T> {
export interface GetComponentProps<T = {}> {
/**
* The path at which to request data,
* typically composed by parent Gets or the RestfulProvider.
Expand All @@ -61,9 +61,11 @@ export interface GetComponentProps<T> {
* A function to resolve data return from the backend, most typically
* used when the backend response needs to be adapted in some way.
*/
resolve?: ResolveFunction<T>;
resolve: ResolveFunction<T>;
/**
* Should we wait until we have data before rendering?
* This is useful in cases where data is available too quickly
* to display a spinner or some type of loading state.
*/
wait?: boolean;
/**
Expand Down Expand Up @@ -96,28 +98,41 @@ export interface GetComponentState<T> {
* debugging.
*/
class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<GetComponentState<T>>> {
private shouldFetchImmediately = () => !this.props.wait && !this.props.lazy;

readonly state: Readonly<GetComponentState<T>> = {
public readonly state: Readonly<GetComponentState<T>> = {
data: null, // Means we don't _yet_ have data.
response: null,
error: "",
loading: this.shouldFetchImmediately(),
loading: false,
};

componentDidMount() {
this.shouldFetchImmediately() && this.fetch();
public static getDerivedStateFromProps(props: Pick<GetComponentProps, "lazy">) {
return { loading: !props.lazy };
}

componentDidUpdate(prevProps: GetComponentProps<T>) {
public static defaultProps = {
resolve: (unresolvedData: any) => unresolvedData,
};

public componentDidMount() {
if (!this.props.lazy) {
this.fetch();
}
}

public componentDidUpdate(prevProps: GetComponentProps<T>) {
// If the path or base prop changes, refetch!
const { path, base } = this.props;
if (prevProps.path !== path || prevProps.base !== base) {
this.shouldFetchImmediately() && this.fetch();
if (!this.props.lazy) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use this.state.loading here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the logic flow here is simply:

  • did something change? 🤔
  • does the user want to lazy load? 🤔
  • DONT DO ANYTHING.

It doesn't quite depend on loading state.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but if I'm in loading, that means that I have a request pending, so 2 (or more) requests can arrive in this case -> carefull to concurrency 💥

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's tackle this in a separate PR. I made an issue (#12) where we can handle this.

this.fetch();
}
}
}

getRequestOptions = (extraOptions?: Partial<RequestInit>, extraHeaders?: boolean | { [key: string]: string }) => {
public getRequestOptions = (
extraOptions?: Partial<RequestInit>,
extraHeaders?: boolean | { [key: string]: string },
) => {
const { requestOptions } = this.props;

if (typeof requestOptions === "function") {
Expand All @@ -143,12 +158,9 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
};
};

fetch = async (requestPath?: string, thisRequestOptions?: RequestInit) => {
const { base, path } = this.props;
public fetch = async (requestPath?: string, thisRequestOptions?: RequestInit) => {
const { base, path, resolve } = this.props;
this.setState(() => ({ error: "", loading: true }));

const { resolve } = this.props;
const foolProofResolve = resolve || (data => data);
const response = await fetch(`${base}${requestPath || path || ""}`, this.getRequestOptions(thisRequestOptions));

if (!response.ok) {
Expand All @@ -159,11 +171,11 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
const data: T =
response.headers.get("content-type") === "application/json" ? await response.json() : await response.text();

this.setState({ loading: false, data: foolProofResolve(data) });
this.setState({ loading: false, data: resolve(data) });
return data;
};

render() {
public render() {
const { children, wait, path, base } = this.props;
const { data, error, loading, response } = this.state;

Expand All @@ -189,9 +201,9 @@ function Get<T>(props: GetComponentProps<T>) {
return (
<RestfulReactConsumer>
{contextProps => (
<RestfulProvider {...contextProps} base={`${contextProps.base}${props.path}`}>
<RestfulReactProvider {...contextProps} base={`${contextProps.base}${props.path}`}>
<ContextlessGet {...contextProps} {...props} />
</RestfulProvider>
</RestfulReactProvider>
)}
</RestfulReactConsumer>
);
Expand Down
12 changes: 6 additions & 6 deletions src/Mutate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import RestfulProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context";

/**
* An enumeration of states that a fetchable
Expand Down Expand Up @@ -70,13 +70,13 @@ export interface MutateComponentState {
* debugging.
*/
class ContextlessMutate extends React.Component<MutateComponentProps, MutateComponentState> {
readonly state: Readonly<MutateComponentState> = {
public readonly state: Readonly<MutateComponentState> = {
response: null,
loading: false,
error: "",
};

mutate = async (body?: string | {}, mutateRequestOptions?: RequestInit) => {
public mutate = async (body?: string | {}, mutateRequestOptions?: RequestInit) => {
const { base, path, verb: method, requestOptions: providerRequestOptions } = this.props;
this.setState(() => ({ error: "", loading: true }));

Expand All @@ -103,7 +103,7 @@ class ContextlessMutate extends React.Component<MutateComponentProps, MutateComp
return response;
};

render() {
public render() {
const { children, path, base } = this.props;
const { error, loading, response } = this.state;

Expand All @@ -125,9 +125,9 @@ function Mutate(props: MutateComponentProps) {
return (
<RestfulReactConsumer>
{contextProps => (
<RestfulProvider {...contextProps} base={`${contextProps.base}${props.path}`}>
<RestfulReactProvider {...contextProps} base={`${contextProps.base}${props.path}`}>
<ContextlessMutate {...contextProps} {...props} />
</RestfulProvider>
</RestfulReactProvider>
)}
</RestfulReactConsumer>
);
Expand Down
51 changes: 29 additions & 22 deletions src/Poll.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from "react";
import { RestfulReactConsumer } from "./Context";
import { RestfulProvider } from ".";
import { GetComponentState, Meta as GetComponentMeta, GetComponentProps } from "./Get";
import { GetComponentProps, GetComponentState, Meta as GetComponentMeta } from "./Get";

/**
* Meta information returned from the poll.
Expand Down Expand Up @@ -47,7 +46,7 @@ interface Actions {
/**
* Props that can control the Poll component.
*/
interface PollProps<T> {
interface PollProps<T = {}> {
/**
* What path are we polling on?
*/
Expand Down Expand Up @@ -127,24 +126,31 @@ interface PollState<T> {
* The <Poll /> component without context.
*/
class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollState<T>>> {
private keepPolling = !this.props.lazy;
readonly state: Readonly<PollState<T>> = {
public readonly state: Readonly<PollState<T>> = {
data: null,
loading: !this.props.lazy,
lastResponse: null,
polling: this.keepPolling,
polling: false,
finished: false,
};

static defaultProps = {
public static getDerivedStateFromProps(props: Pick<PollProps, "lazy">) {
return {
polling: !props.lazy,
};
}

public static defaultProps = {
interval: 1000,
resolve: (data: any) => data,
};

private keepPolling = !this.props.lazy;

/**
* This thing does the actual poll.
*/
cycle = async () => {
public cycle = async () => {
// Have we stopped?
if (!this.keepPolling) {
return; // stop.
Expand Down Expand Up @@ -172,22 +178,23 @@ class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollStat
data: resolve ? resolve(responseBody) : responseBody,
}));

await new Promise(resolve => setTimeout(resolve, interval)); // Wait for interval to pass.
// Wait for interval to pass.
await new Promise(resolvePromise => setTimeout(resolvePromise, interval));
this.cycle(); // Do it all again!
};

start = async () => {
public start = async () => {
this.keepPolling = true;
this.setState(() => ({ polling: true })); // let everyone know we're done here.}
this.cycle();
};

stop = async () => {
public stop = async () => {
this.keepPolling = false;
this.setState(() => ({ polling: false, finished: true })); // let everyone know we're done here.}
};

componentDidMount() {
public componentDidMount() {
const { path, lazy } = this.props;

if (!path) {
Expand All @@ -196,14 +203,16 @@ class ContextlessPoll<T> extends React.Component<PollProps<T>, Readonly<PollStat
);
}

!lazy && this.start();
if (!lazy) {
this.start();
}
}

componentWillUnmount() {
public componentWillUnmount() {
this.stop();
}

render() {
public render() {
const { lastResponse: response, data, polling, loading, error, finished } = this.state;
const { children, base, path } = this.props;

Expand Down Expand Up @@ -233,13 +242,11 @@ function Poll<T>(props: PollProps<T>) {
return (
<RestfulReactConsumer>
{contextProps => (
<RestfulProvider {...contextProps} base={`${contextProps.base}${props.path}`}>
<ContextlessPoll
{...contextProps}
{...props}
requestOptions={{ ...contextProps.requestOptions, ...props.requestOptions }}
/>
</RestfulProvider>
<ContextlessPoll
{...contextProps}
{...props}
requestOptions={{ ...contextProps.requestOptions, ...props.requestOptions }}
/>
)}
</RestfulReactConsumer>
);
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export { default as Poll } from "./Poll";
export { default as Mutate } from "./Mutate";

export { Get };

export default Get;
24 changes: 23 additions & 1 deletion tslint.json
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
{}
{
"extends": ["tslint:recommended", "tslint-config-prettier", "tslint-plugin-blank-line"],
"rules": {
"blank-line": true,
"one-variable-per-declaration": false,
"semicolon": false,
"quotemark": "double",
"variable-name": ["allow-pascal-case"],
"indent": false,
"import-name": false,
"object-literal-sort-keys": false,
"interface-name": false,
"no-console": [true, "log", "error"],
"no-unused-expression": true,
"no-implicit-dependencies": [true, "dev"],
"no-unused-variable": [true, { "check-parameters": true }],
"no-duplicate-imports": true,
"completed-docs": false,
"import-spacing": true,
"match-default-export-name": true,
"member-ordering": false
}
}