- adds `LOCAL_STORAGE_KEY` to `.env`, to allow saving/retrieving MobX store state to `localStorage` on the client - adds `schema/schema.graphql`, with sample schema for Hacker News stories - refactors `<StateConsumer>` as new `withStore()` HOC in `@/lib/store.ts` - refactors `<HackerNews>` example to use the new auto-generayed `<GetHackerNewsTopStories.Component>` - removes default exports in example components - fixes #150 - React-Hot-Loader 4.7 - renames `@/data/state.ts` -> `@/data/store.ts` (now exports `Store` class) - adds `autosave` and new `rehydrate` funcs in `@/lib/store.ts`, for saving and retrieving MobX state to/from `localStorage` - refactors `@/entry/client.tsx` with new MobX store hydration/auto-save - refactors `@/entry/server.tsx` with new MobX store - adds auto-generated `@/graphql/index.tsx` with query and HOC for getting top HackerNews stories - removes previous `@/lib/mobx.ts` - fixes static runner; removes `koa2-history-api-fallback` package in favour of a local function - adds `HOST` to `.env` and `common.host` in runner to allow binding to non-`localhost` - adds `react-dom` alias to Webpack common config, to use `@hot-loader/react-dom`pull/162/head
parent
31a95bf5c6
commit
a6abe69095
@ -1,2 +1,4 @@ |
||||
HOST=0.0.0.0 |
||||
GRAPHQL=https://graphqlhub.com/graphql |
||||
WS_SUBSCRIPTIONS=0 |
||||
WS_SUBSCRIPTIONS=0 |
||||
LOCAL_STORAGE_KEY=reactql |
@ -0,0 +1,9 @@ |
||||
overwrite: true |
||||
schema: "schema/schema.graphql" |
||||
documents: "src/**/*.graphql" |
||||
generates: |
||||
src/graphql/index.tsx: |
||||
plugins: |
||||
- "typescript-common" |
||||
- "typescript-client" |
||||
- "typescript-react-apollo" |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,17 @@ |
||||
type Story { |
||||
id: String |
||||
title: String |
||||
url: String |
||||
} |
||||
|
||||
type HackerNews { |
||||
topStories: [Story] |
||||
} |
||||
|
||||
type Query { |
||||
hn: HackerNews |
||||
} |
||||
|
||||
schema { |
||||
query: Query |
||||
} |
@ -1,6 +1,6 @@ |
||||
import { action, observable } from "mobx"; |
||||
|
||||
export class State { |
||||
export class Store { |
||||
@observable count = 0; |
||||
@action public increment = () => { |
||||
this.count = this.count + 1; |
@ -0,0 +1,85 @@ |
||||
export type Maybe<T> = T | null; |
||||
|
||||
// ====================================================
|
||||
// Documents
|
||||
// ====================================================
|
||||
|
||||
export namespace GetHackerNewsTopStories { |
||||
export type Variables = {}; |
||||
|
||||
export type Query = { |
||||
__typename?: "Query"; |
||||
|
||||
hn: Maybe<Hn>; |
||||
}; |
||||
|
||||
export type Hn = { |
||||
__typename?: "HackerNews"; |
||||
|
||||
topStories: Maybe<(Maybe<TopStories>)[]>; |
||||
}; |
||||
|
||||
export type TopStories = { |
||||
__typename?: "Story"; |
||||
|
||||
id: Maybe<string>; |
||||
|
||||
title: Maybe<string>; |
||||
|
||||
url: Maybe<string>; |
||||
}; |
||||
} |
||||
|
||||
import * as ReactApollo from "react-apollo"; |
||||
import * as React from "react"; |
||||
|
||||
import gql from "graphql-tag"; |
||||
|
||||
// ====================================================
|
||||
// Components
|
||||
// ====================================================
|
||||
|
||||
export namespace GetHackerNewsTopStories { |
||||
export const Document = gql` |
||||
query GetHackerNewsTopStories { |
||||
hn { |
||||
topStories { |
||||
id |
||||
title |
||||
url |
||||
} |
||||
} |
||||
} |
||||
`;
|
||||
export class Component extends React.Component< |
||||
Partial<ReactApollo.QueryProps<Query, Variables>> |
||||
> { |
||||
render() { |
||||
return ( |
||||
<ReactApollo.Query<Query, Variables> |
||||
query={Document} |
||||
{...(this as any)["props"] as any} |
||||
/> |
||||
); |
||||
} |
||||
} |
||||
export type Props<TChildProps = any> = Partial< |
||||
ReactApollo.DataProps<Query, Variables> |
||||
> & |
||||
TChildProps; |
||||
export function HOC<TProps, TChildProps = any>( |
||||
operationOptions: |
||||
| ReactApollo.OperationOption< |
||||
TProps, |
||||
Query, |
||||
Variables, |
||||
Props<TChildProps> |
||||
> |
||||
| undefined |
||||
) { |
||||
return ReactApollo.graphql<TProps, Query, Variables, Props<TChildProps>>( |
||||
Document, |
||||
operationOptions |
||||
); |
||||
} |
||||
} |
@ -1,44 +0,0 @@ |
||||
// MobX helpers
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// IMPORTS
|
||||
|
||||
import * as React from "react"; |
||||
import { runInAction } from "mobx"; |
||||
import { Observer } from "mobx-react"; |
||||
|
||||
/* Local */ |
||||
import { State } from "@/data/state"; |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
interface IStateConsumerProps { |
||||
children: (state: State) => React.ReactElement<any>; |
||||
} |
||||
|
||||
// State context, for propogating state down a React chain
|
||||
const StateContext = React.createContext<State>(new State()); |
||||
|
||||
// Export a <StateProvider>, for overridding default state
|
||||
export const { Provider: StateProvider } = StateContext; |
||||
|
||||
// State HOC for both receiving and observing state
|
||||
export const StateConsumer: React.StatelessComponent<IStateConsumerProps> = ({ |
||||
children |
||||
}) => ( |
||||
<StateContext.Consumer> |
||||
{state => { |
||||
return <Observer>{() => children(state)}</Observer>; |
||||
}} |
||||
</StateContext.Consumer> |
||||
); |
||||
|
||||
// Rehydrate JSON state to MobX
|
||||
export function rehydrate(state: State) { |
||||
const s = (window as any).__STATE__; |
||||
if (typeof s === "object") { |
||||
runInAction(() => { |
||||
Object.keys(s).forEach(key => ((state as any)[key] = s[key])); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,55 @@ |
||||
// MobX helpers
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// IMPORTS
|
||||
|
||||
import * as React from "react"; |
||||
import { runInAction, autorun, toJS } from "mobx"; |
||||
import { observer, inject } from "mobx-react"; |
||||
|
||||
/* Local */ |
||||
import { Store } from "@/data/store"; |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type WithStore<T> = T & { |
||||
store: Store; |
||||
}; |
||||
|
||||
export function withStore<P>( |
||||
Component: React.ComponentType<WithStore<P>>, |
||||
): React.ComponentType<P> { |
||||
return inject<{ store: Store }, {}, {}, {}>(stores => ({ |
||||
store: stores.store, |
||||
}))(observer(Component as any)); |
||||
} |
||||
|
||||
// CLIENT-ONLY: Rehydrate JSON state to MobX
|
||||
export function rehydrate(store: Store) { |
||||
// Helper to parse and rehydrate the store
|
||||
const init = (data: any) => { |
||||
if (typeof data === "object") { |
||||
Object.keys(data).forEach(key => ((store as any)[key] = data[key])); |
||||
} |
||||
}; |
||||
|
||||
// Perform the rehydration atomically
|
||||
runInAction(() => { |
||||
init((window as any).__STORE__); |
||||
try { |
||||
const parsed = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)!); |
||||
if (parsed) { |
||||
init(parsed); |
||||
} |
||||
} catch (_) { |
||||
/* Ignore localStorage parse errors */ |
||||
} |
||||
}); |
||||
} |
||||
|
||||
// CLIENT-ONLY: Save store changes to localStorage
|
||||
export function autosave(store: Store) { |
||||
autorun(() => { |
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(toJS(store))); |
||||
}); |
||||
} |
@ -0,0 +1,9 @@ |
||||
query GetHackerNewsTopStories { |
||||
hn { |
||||
topStories { |
||||
id |
||||
title |
||||
url |
||||
} |
||||
} |
||||
} |
@ -1,26 +0,0 @@ |
||||
// Get the top stories from HackerNews
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// IMPORTS
|
||||
|
||||
/* NPM */ |
||||
|
||||
// GraphQL tag library, for creating GraphQL queries from plain
|
||||
// template text
|
||||
import gql from "graphql-tag"; |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GraphQL query for retrieving Hacker News top-stories from the
|
||||
// https://graphqlhub.com/playground sample server endpoint
|
||||
export default gql` |
||||
{ |
||||
hn { |
||||
topStories { |
||||
id |
||||
title |
||||
url |
||||
} |
||||
} |
||||
} |
||||
`;
|
@ -1,3 +1,4 @@ |
||||
declare var GRAPHQL: string; |
||||
declare var SERVER: boolean; |
||||
declare var WS_SUBSCRIPTIONS: boolean; |
||||
declare var LOCAL_STORAGE_KEY: string; |
||||
|
Loading…
Reference in new issue