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?
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
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.
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 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 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.
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