Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/easycz/grpc-web-hacker-news
An example app implementing a Hacker News reader in React with gRPC-Web and Go backend
https://github.com/easycz/grpc-web-hacker-news
example golang grpc grpc-web react redux typescript
Last synced: 1 day ago
JSON representation
An example app implementing a Hacker News reader in React with gRPC-Web and Go backend
- Host: GitHub
- URL: https://github.com/easycz/grpc-web-hacker-news
- Owner: easyCZ
- License: mit
- Created: 2018-01-13T22:25:04.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2023-03-06T22:33:40.000Z (almost 2 years ago)
- Last Synced: 2025-01-13T03:07:31.623Z (9 days ago)
- Topics: example, golang, grpc, grpc-web, react, redux, typescript
- Language: TypeScript
- Homepage:
- Size: 10 MB
- Stars: 386
- Watchers: 9
- Forks: 59
- Open Issues: 9
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
## grpc-web-hacker-news
An example app implementing a Hacker News reader. This example aims to demonstrate usage of [grpc-web](https://github.com/improbable-eng/grpc-web)(v0.5.0) with React. It additionally shows how to integrate with Redux.### Running
To start both the Go backend server and the frontend React application, run the following:
```bash
./start.sh
```![Screenshot](https://raw.githubusercontent.com/easyCZ/grpc-web-hacker-news/master/screenshots/grpc-web-hacker-news.png)
The backend server is running on `http://localhost:8900` while the frontend will by default start on `http://localhost:3000`
## Contributing
Contributions are welcome, please open an Issue or raise a PR.## Notable setup points
### Disable TSLint for protobuf generated classes
Gernerated proto classes do not confirm to TS lint requirements, [disable linting](https://github.com/easyCZ/grpc-web-hacker-news/blob/master/app/tslint.json#L4)
```json
{
"linterOptions": {
"exclude": [
"src/proto/*"
]
},
}
```### Configure protobuf compiler script
In this example, we're using a `protoc.sh` script to aid compilation
```bash
protoc \
--go_out=plugins=grpc:./server \
--plugin=protoc-gen-ts=./app/node_modules/.bin/protoc-gen-ts \
--ts_out=service=true:./app/src \
--js_out=import_style=commonjs,binary:./app/src \
./proto/hackernews.proto
```### Generated proto classes with Redux
Redux favors plain objects over object instances. This complicates usage of the generated proto classes, in this example class [`Story`](https://github.com/easyCZ/grpc-web-hacker-news/blob/master/app/src/proto/hackernews_pb.d.ts#L6). In order to use the generated classes with redux, we must use the `Story.AsObject` type which is the plain object representation. For example our reducer should look like this:```js
export type StoryState = {
readonly stories: { [storyId: number]: Story.AsObject },
readonly error: Error | null,
readonly loading: boolean,
readonly selected: Story.AsObject | null,
};export default function (state: StoryState = initialState, action: RootAction): StoryState {
switch (action.type) {
case ADD_STORY:
const story: Story.AsObject = action.payload.toObject();
const selected = state.selected !== null ? state.selected : story;
if (story.id && story.id) {
return {
...state,
loading: false,
stories: {...state.stories, [story.id]: story},
selected,
};
}
return state;default:
return state;
}}
```
Note the usage of `Story.AsObject` rather than just `Story`### gRPC-Web Redux Middleware
An [example redux middleware is included](https://github.com/easyCZ/grpc-web-hacker-news/blob/master/app/src/middleware/grpc.ts).
```js
import { Action, Dispatch, Middleware, MiddlewareAPI } from 'redux';
import { Code, grpc, Metadata, Transport } from 'grpc-web-client';
import * as jspb from 'google-protobuf';const GRPC_WEB_REQUEST = 'GRPC_WEB_REQUEST';
// const GRPC_WEB_INVOKE = 'GRPC_WEB_INVOKE';// Descriptor of a grpc-web payload
// life-cycle methods mirror grpc-web but allow for an action to be dispatched when triggered
export type GrpcActionPayload = {
// The method descriptor to use for a gRPC request, equivalent to grpc.invoke(methodDescriptor, ...)
methodDescriptor: grpc.MethodDefinition,
// The transport to use for grpc-web, automatically selected if empty
transport?: Transport,
// toggle debug messages
debug?: boolean,
// the URL of a host this request should go to
host: string,
// An instance of of the request message
request: RequestType,
// Additional metadata to attach to the request, the same as grpc-web
metadata?: Metadata.ConstructorArg,
// Called immediately before the request is started, useful for toggling a loading status
onStart?: () => Action | void,
// Called when response headers are received
onHeaders?: (headers: Metadata) => Action | void,
// Called on each incoming message
onMessage?: (res: ResponseType) => Action | void,
// Called at the end of a request, make sure to check the exit code
onEnd: (code: Code, message: string, trailers: Metadata) => Action | void,
};// Basic type for a gRPC Action
export type GrpcAction = {
type: typeof GRPC_WEB_REQUEST,
payload: GrpcActionPayload,
};// Action creator, Use it to create a new grpc action
export function grpcRequest(
payload: GrpcActionPayload
): GrpcAction {
return {
type: GRPC_WEB_REQUEST,
payload,
};
}export function newGrpcMiddleware(): Middleware {
return ({getState, dispatch}: MiddlewareAPI<{}>) => (next: Dispatch<{}>) => (action: any) => {
// skip non-grpc actions
if (!isGrpcWebUnaryAction(action)) {
return next(action);
}const payload = action.payload;
if (payload.onStart) {
payload.onStart();
}grpc.invoke(payload.methodDescriptor, {
debug: payload.debug,
host: payload.host,
request: payload.request,
metadata: payload.metadata,
transport: payload.transport,
onHeaders: headers => {
if (!payload.onHeaders) { return; }
const actionToDispatch = payload.onHeaders(headers);
return actionToDispatch && dispatch(actionToDispatch);
},
onMessage: res => {
if (!payload.onMessage) { return; }
const actionToDispatch = payload.onMessage(res);
return actionToDispatch && dispatch(actionToDispatch);
},
onEnd: (code, msg, trailers) => {
const actionToDispatch = payload.onEnd(code, msg, trailers);
return actionToDispatch && dispatch(actionToDispatch);
},
});return next(action);
};
}function isGrpcWebUnaryAction(action: any): action is GrpcAction {
return action && action.type && action.type === GRPC_WEB_REQUEST && isGrpcWebPayload(action);
}function isGrpcWebPayload(action: any): boolean {
return action &&
action.payload &&
action.payload.methodDescriptor &&
action.payload.request &&
action.payload.onEnd &&
action.payload.host;
}```