Skip to content

Instantly share code, notes, and snippets.

@ryanswrt

ryanswrt/blog.md Secret

Created July 25, 2019 04:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ryanswrt/2617a7c7ce4122099797b36874ed59ed to your computer and use it in GitHub Desktop.
Save ryanswrt/2617a7c7ce4122099797b36874ed59ed to your computer and use it in GitHub Desktop.

Using React Hooks to wrap connectors to live data sources

At Perlin, we want to make creating Dapps as easy as possible. In our previous chat Dapp example, we had to write around 100 lines of connector glue in order to connect to the network, get the account status, and load a contract.

As the example was written with React Hooks, extracting that glue into re-usable hooks was easy, and allows us to connect to Wavelet in a reactive way with only 3 lines of code.

https://gist.github.com/1718c49b8015aca81ad6495e43d49e3f

Here is a full example that allows you to "Yo" on the blockchain.

https://gist.github.com/931a58a41cad79ed8b341fca8d9e46ac

Much simpler than the 300 lines in the previous example, right?

Why Hooks

This highlights a key strength in the design of React Hooks, if we wrote the logic as a traditional component, we would have to build a number of HOCs and nest our actual business logic component deep within this stack. If we want to use multiple components, clients, or accounts, we would have to restructure this nesting. Components would also impose interface requirements on their children; require messy render callback code, or use React Context which can make components less reusable as it creates an implicit dependency on a parent component.

Here's how it would look like if we used any of those approaches https://gist.github.com/fcfa065156e2cac9b9446b8375f8518e

This assumes that we always pass down the client to all children components, and creates messy intermediate functions to pass injected dependencies to the base component.

https://gist.github.com/efeb7a62e3ebf961b5e2a90e0c09d645

This example is similar to the HOC example, and is more explicit in how dependencies get passed, but having 2 different forms of nesting in your component gets ugly quick.

https://gist.github.com/c79c2e0d692bbf2483741ea9c7e668b9

The context example looks the cleanest, but mainly because we hide the dependencies being passed down in a context object. Now MyComponent has to ensure it only updates when relevant parts of the context data has changed, and is tightly coupled to its parent component.

As we can see, all of these approaches have downsides and different implementation concerns. The React Hooks approach is self contained, yet composed of reusable elements, simplifying the amount of dependency plumbing that would otherwise be in JSX.

There are a concerns to keep in mind when writing hooks however:

  • Preventing expensive code from running on every render
  • Dependency loops / infinite renders
  • Error handling
  • Testing

Preventing expensive code from running on every render

When writing data connectors, the most expensive functions are generally those that call the external services; in our case when we verify that the client we created is valid by fetching the node info,

https://gist.github.com/06791a4c3ccfd1bfcf63d00ef8713c96

when we fetch account details and register its live update socket,

https://gist.github.com/d5a5a3901cfd4c2554c0c89a3000fe8e

and when we initialize contracts:

https://gist.github.com/1589f568ba1a247e4a16b5e164dc2fdb

These actions should always be wrapped in useEffect hooks, which will cause them to only render when a value in their dependency array changes. In order to avoid subtle bugs, all values used in the effect callback need to be present in the dependency array, and the handy eslint-plugin-react-hooks ensures that you do this.

Preventing dependency cycles

If you follow the eslint-plugin-react-hooks rule, but do not pay attention to the variables being used, you may end up including values that get set by the effect itself, leading to infinite renders.

There are even more subtle cases of these dependency loops, where a component that uses a hook provides a value to that hook that gets updated when the hook returns a value eg. say we wanted to build a component that only counts when we get permission from the backend.

https://gist.github.com/6090ed8e71ce0c5db31dcadf97e3bc7d

Here you don't immediately see the dependency loop, as the only state change in the effect is permission, which isn't a dependency, but we still end up with an infinite loop as count's value will change on every render, which in turn will cause a the useEffect in usePermissionedCounter to trigger, which will cause MyComp to re-render, starting the cycle again.

While this example seems a bit strained, we experienced it when writing a hook that takes a callback to handle live updates. The solution is to ensure that callbacks being passed to hooks are always wrapped in a useCallback hook, with appropriate dependency arrays. Would be pretty neat if we could have an eslint rule for this as well!

Error handling

Error handling in async hooks is another consideration, as you cannot simply throw and catch or use React Error Boundaries. Instead, you should always provide an error value in the result array if any of the promises in your async code can be rejected. You can then check whether the error is not-null and handle it in the components that use it.

Also remember to unset previous error / result values correctly, otherwise you may end up never recovering from an error or seeing old values from a hook that has subsequently failed!

https://gist.github.com/3aaa353bb0fce285c479f71079478007

Testing

Testing React hooks is relatively simple, using jest and @testing-library/react-hooks. A quick example would be

https://gist.github.com/69ca377dd94dc37f6efeb3ac72b3fd36

Writing tests also gives you deeper insights into exactly how your component behaves, that you may not directly see in your app, for example, the implicit requirement that you need to wait for the async hook to execute may not be obvious if the promise resolves immediately. You can also identify inefficient renders when you need to waitForNextUpdate multiple times to achieve the desired state.

Conclusion

Overall, extracting the hooks has resulted in much more concise code, with behaviour that can be tested and made more robust without having to change our app's implementation. You definitely have to understand reactive cycles in greater depth to avoid critical errors, but it is generally better practise to have apps that are clearly broken, instead of subtly broken, and Hooks will expose these issues much quicker.

The resulting package is available at https://github.com/ryanswart/react-use-wavelet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment