{"id":16879646,"url":"https://github.com/easycz/grpc-web-hacker-news","last_synced_at":"2025-04-06T01:08:25.296Z","repository":{"id":42513293,"uuid":"117383278","full_name":"easyCZ/grpc-web-hacker-news","owner":"easyCZ","description":"An example app implementing a Hacker News reader in React with gRPC-Web and Go backend","archived":false,"fork":false,"pushed_at":"2023-03-06T22:33:40.000Z","size":10526,"stargazers_count":388,"open_issues_count":9,"forks_count":59,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-03-29T23:11:32.047Z","etag":null,"topics":["example","golang","grpc","grpc-web","react","redux","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/easyCZ.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-01-13T22:25:04.000Z","updated_at":"2025-02-14T12:05:27.000Z","dependencies_parsed_at":"2024-01-07T16:06:44.769Z","dependency_job_id":"e03c1b3a-2503-42cd-9bdf-6d4cdd1b6bcc","html_url":"https://github.com/easyCZ/grpc-web-hacker-news","commit_stats":{"total_commits":55,"total_committers":3,"mean_commits":"18.333333333333332","dds":0.6,"last_synced_commit":"4368f615c62eb43a80011f401bbff1e6e92ced2e"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easyCZ%2Fgrpc-web-hacker-news","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easyCZ%2Fgrpc-web-hacker-news/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easyCZ%2Fgrpc-web-hacker-news/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easyCZ%2Fgrpc-web-hacker-news/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/easyCZ","download_url":"https://codeload.github.com/easyCZ/grpc-web-hacker-news/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247419860,"owners_count":20936012,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["example","golang","grpc","grpc-web","react","redux","typescript"],"created_at":"2024-10-13T15:55:11.741Z","updated_at":"2025-04-06T01:08:25.266Z","avatar_url":"https://github.com/easyCZ.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## grpc-web-hacker-news\nAn 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.\n\n### Running\nTo start both the Go backend server and the frontend React application, run the following:\n```bash\n./start.sh\n```\n\n![Screenshot](https://raw.githubusercontent.com/easyCZ/grpc-web-hacker-news/master/screenshots/grpc-web-hacker-news.png)\n\n\nThe backend server is running on `http://localhost:8900` while the frontend will by default start on `http://localhost:3000`\n\n## Contributing\nContributions are welcome, please open an Issue or raise a PR.\n\n## Notable setup points\n\n### Disable TSLint for protobuf generated classes\nGernerated 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)\n```json\n{\n    \"linterOptions\": {\n        \"exclude\": [\n            \"src/proto/*\"\n        ]\n    },\n}\n```\n\n### Configure protobuf compiler script\nIn this example, we're using a `protoc.sh` script to aid compilation\n```bash\nprotoc \\\n    --go_out=plugins=grpc:./server \\\n    --plugin=protoc-gen-ts=./app/node_modules/.bin/protoc-gen-ts \\\n    --ts_out=service=true:./app/src \\\n    --js_out=import_style=commonjs,binary:./app/src \\\n    ./proto/hackernews.proto\n```\n\n### Generated proto classes with Redux\nRedux 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:\n\n```js\nexport type StoryState = {\n  readonly stories: { [storyId: number]: Story.AsObject },\n  readonly error: Error | null,\n  readonly loading: boolean,\n  readonly selected: Story.AsObject | null,\n};\n\nexport default function (state: StoryState = initialState, action: RootAction): StoryState {\n\n  switch (action.type) {\n\n    case ADD_STORY:\n      const story: Story.AsObject = action.payload.toObject();\n      const selected = state.selected !== null ? state.selected : story;\n      if (story.id \u0026\u0026 story.id) {\n        return {\n          ...state,\n          loading: false,\n          stories: {...state.stories, [story.id]: story},\n          selected,\n        };\n      }\n      return state;\n\n    default:\n      return state;\n  }\n\n}\n```\nNote the usage of `Story.AsObject` rather than just `Story`\n\n### gRPC-Web Redux Middleware\nAn [example redux middleware is included](https://github.com/easyCZ/grpc-web-hacker-news/blob/master/app/src/middleware/grpc.ts).\n```js\nimport { Action, Dispatch, Middleware, MiddlewareAPI } from 'redux';\nimport { Code, grpc, Metadata, Transport } from 'grpc-web-client';\nimport * as jspb from 'google-protobuf';\n\nconst GRPC_WEB_REQUEST = 'GRPC_WEB_REQUEST';\n// const GRPC_WEB_INVOKE = 'GRPC_WEB_INVOKE';\n\n// Descriptor of a grpc-web payload\n// life-cycle methods mirror grpc-web but allow for an action to be dispatched when triggered\nexport type GrpcActionPayload\u003cRequestType extends jspb.Message, ResponseType extends jspb.Message\u003e = {\n  // The method descriptor to use for a gRPC request, equivalent to grpc.invoke(methodDescriptor, ...)\n  methodDescriptor: grpc.MethodDefinition\u003cRequestType, ResponseType\u003e,\n  // The transport to use for grpc-web, automatically selected if empty\n  transport?: Transport,\n  // toggle debug messages\n  debug?: boolean,\n  // the URL of a host this request should go to\n  host: string,\n  // An instance of of the request message\n  request: RequestType,\n  // Additional metadata to attach to the request, the same as grpc-web\n  metadata?: Metadata.ConstructorArg,\n  // Called immediately before the request is started, useful for toggling a loading status\n  onStart?: () =\u003e Action | void,\n  // Called when response headers are received\n  onHeaders?: (headers: Metadata) =\u003e Action | void,\n  // Called on each incoming message\n  onMessage?: (res: ResponseType) =\u003e Action | void,\n  // Called at the end of a request, make sure to check the exit code\n  onEnd: (code: Code, message: string, trailers: Metadata) =\u003e Action | void,\n};\n\n// Basic type for a gRPC Action\nexport type GrpcAction\u003cRequestType extends jspb.Message, ResponseType extends jspb.Message\u003e = {\n  type: typeof GRPC_WEB_REQUEST,\n  payload: GrpcActionPayload\u003cRequestType, ResponseType\u003e,\n};\n\n// Action creator, Use it to create a new grpc action\nexport function grpcRequest\u003cRequestType extends jspb.Message, ResponseType extends jspb.Message\u003e(\n  payload: GrpcActionPayload\u003cRequestType, ResponseType\u003e\n): GrpcAction\u003cRequestType, ResponseType\u003e {\n  return {\n    type: GRPC_WEB_REQUEST,\n    payload,\n  };\n}\n\nexport function newGrpcMiddleware(): Middleware {\n  return ({getState, dispatch}: MiddlewareAPI\u003c{}\u003e) =\u003e (next: Dispatch\u003c{}\u003e) =\u003e (action: any) =\u003e {\n    // skip non-grpc actions\n    if (!isGrpcWebUnaryAction(action)) {\n      return next(action);\n    }\n\n    const payload = action.payload;\n\n    if (payload.onStart) {\n      payload.onStart();\n    }\n\n    grpc.invoke(payload.methodDescriptor, {\n      debug: payload.debug,\n      host: payload.host,\n      request: payload.request,\n      metadata: payload.metadata,\n      transport: payload.transport,\n      onHeaders: headers =\u003e {\n        if (!payload.onHeaders) { return; }\n        const actionToDispatch = payload.onHeaders(headers);\n        return actionToDispatch \u0026\u0026 dispatch(actionToDispatch);\n      },\n      onMessage: res =\u003e {\n        if (!payload.onMessage) { return; }\n        const actionToDispatch = payload.onMessage(res);\n        return actionToDispatch \u0026\u0026 dispatch(actionToDispatch);\n      },\n      onEnd: (code, msg, trailers) =\u003e {\n        const actionToDispatch = payload.onEnd(code, msg, trailers);\n        return actionToDispatch \u0026\u0026 dispatch(actionToDispatch);\n      },\n    });\n\n    return next(action);\n  };\n}\n\nfunction isGrpcWebUnaryAction(action: any): action is GrpcAction\u003cjspb.Message, jspb.Message\u003e {\n  return action \u0026\u0026 action.type \u0026\u0026 action.type === GRPC_WEB_REQUEST \u0026\u0026 isGrpcWebPayload(action);\n}\n\nfunction isGrpcWebPayload(action: any): boolean {\n  return action \u0026\u0026\n    action.payload \u0026\u0026\n    action.payload.methodDescriptor \u0026\u0026\n    action.payload.request \u0026\u0026\n    action.payload.onEnd \u0026\u0026\n    action.payload.host;\n}\n\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasycz%2Fgrpc-web-hacker-news","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feasycz%2Fgrpc-web-hacker-news","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasycz%2Fgrpc-web-hacker-news/lists"}