Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Apollo is the best solution for managing remote data with a client cache. But if you are already using Apollo (or another client cache like Relay), the question is: What should you do about the rest of your client state? Reactâs new context API (available in 16.3 or as a polyfill) has opened up new possibilities for dealing with app state. At OK GROW!, weâve been trying to find that blissful state that combines the data from the server, component state, and the app state, which hasnât always been smooth.
App state is tiny
I became interested in Redux after watching Dan Abramovâs seminal talk at ReactEurope. After Iâd learned the basics of Redux, I began looking for information on âadvanced Redux.â But that brought me face-to-face with the problem with Redux for Apollo users: Every tutorial I found turned out to be implementing a client cache. You would learn the three or so ways to do async with Redux and then implement those async actions to work with your Redux store. But Apolloâs client cache already does all of that. It has all of my loading and error states handled, it deals with optimistic updates, you name it. So whatâs left to manage?
The answer, as it turns out, is very little for most apps. Most of it should just live in React components.
What about centralizing to Redux for better testability?
I agree that this makes testing very easy, but I have two problems with it:
- It breaks encapsulation. The whole point of having a component system is for encapsulation. React allows you to put state into your components so that when you are working elsewhere, you donât have to think about whatâs happening in that component. As soon as you try to separate that functionality, you have keep two things in your head, the component and the state (which is now probably in a separate file, so you need more buffers open). Not only that, but there is also risk that somebody else (never you!) would be tempted to grab that state from a different component.
- Removing state from components feels un-React-y. Iâve heard of developers making their entire app out of functional components with all state coming from Redux. Hey, I had a âletâs use all functional components!â phase too. But having a global store bypasses the top-down data flow and makes your app harder to reason about. Also, thatâs a lot more code!
Some of this movement stems from the fact that testing components has been pretty awkward for a long time, but I think we can solve that using tools like React Test Renderer (more on this in a future post), or possibly a new tool like Cosmos.
There are apps that will really benefit from Redux, so Iâm not saying not to use it. But in my own work, I have not been able to justify the overhead once Apollo took over remote data synchronization and caching.
What state is left to manage?
If you put most of your state into components and have a client cache, whatâs left? Almost nothing. In one app, I had only a single global state variable to manage. But I tried managing it directly with the old Context API, and that convinced me that app-wide state is still a problem worth solving! Here are some things I think legitimately belong in app-wide state:
- State that needs to be shared between routes. Top-down data propagation runs into a roadblock when you hit a routing boundary. Of course youâll pass some params to dependent routes, but passing shared state is a recipe for disaster. Just use app state instead.
- State that is used all over the place. Sure you could put it in your root component and pass it down, but why? As an aside, I think a case can be made to store current session information (e.g., is the user logged in) in state even though you can also do that with Apollo.
In the last app I worked on, we tried Apollo Link State. The logic is compelling: Since you already have a nice library to query and update values in the database, and since most of your data will already be in that format, why not just store your app state the same way? Then you have one way to do things, and you can use the existing reactive query components to update UI elements regardless of whether the data is local or remote.
To be fair, Apollo Link State totally works, though I ran into some sharp edges on what was then a new addition to Apollo. The real problem is that the way you produce and consume local app data just doesnât match the pattern of remote data. Queries for remote data tend to be specific to one part of your app and donât repeat much. The queries are intentionally verbose to clarify the relationship between the client and server, and they may reside in their own files and may have options for optimistic updates, updating based on mutations, etc. They also make extensive use of loading/error state, the ability to select which fields you need, etc. But by definition, app data is ubiquitous and simpleâââusually key-value pairs, or sometimes objects that are needed in various combinations that are read and updated in multiple places in your app. You donât need to select fields, deal with loading state, etc. Writing queries for these things is simply awkward and excessively verbose, and donât forget that queries and mutations require separate code.
Hereâs an example: We had an app for ordering from restaurants. If you leave your table, it should clear incomplete orders and remove the table number you were sitting at. But you shouldnât be able to do that if you have an unpaid tab (ticket). We had components with render props to compose in the state queries and mutations, and this was a piece of code that appeared in the app:
<ClearOrder render={clearOrder => ( <SetCurrentTable render={setCurrentTable => ( <GetOrderStatus render={({ ticketId }) => ( <HeaderButton label="Leave Table" disabled={!!ticketId} onPress={() => { clearOrder(); setCurrentTable({ qrCode: '' }); }} /> )} /> )} /> )} />
Itâs almost enough to make you go back to higher order components! đ± Actually, I think HOCs would be a good solution here, but composing individual operations is still pretty heavy.
Enter Unstated
Before we look at Unstated, I should say that you donât need to use any other state library at all! The new Context API is quite nice and may be all you need, allowing you to keep state in a root component. That said, hereâs that same component written with Unstated
<Subscribe to={[Table, Order]}> {(currentTable, order) => ( <HeaderButton label="Leave Table" disabled={!!order.state.ticketId} onPress={() => { order.clear(); currentTable.set({ qrCode: '' }); }} /> )}</Subscribe>
I find it much more readable, and there are fewer levels of indentation and fewer lines of code. It avoids the pyramid of doom in two ways:
- Get and set functionality can be shared in one state object, reducing imports. Order and table state could have been combined to one object if we preferred that. We are dealing with classes, so we can combine as many things as we like on that container.
- Thereâs a built-in way to subscribe to multiple state containers in one Subscribe component â the to prop takes an array of state objects.
Finally, if we compare the code to perform clearOrder, one of the actions, the difference becomes a little starker:
The GraphQL is simple enough but doesnât tell us much:
mutation clearOrder { clearOrder @client}
And the resolver ends up looking a lot like a Redux reducer.
clearOrder: (_, __, { cache }) => { const data = cache.readQuery({ query: GET_ORDER });// make sure you mutate your object correctly// or other state could be affected data.items = []; cache.writeQuery({ query: GET_ORDER, data }); return null; // if you forget to return null, you'll get errors},
The equivalent method in the Unstated class would be a one-liner:
// make sure you bind your method if you might pass it// as a propclear = () => this.setState({ items: [] });
Ah, That feels better!
Testing with unstated
Because Unstated creates an object that is not a react component, you can test it without rendering the component just as you might with Redux:
test('clear order', () => {// instantiate and set up our state object for the test const orderState = new OrderState(); orderState.setState({ items: ['hamburger'] }); orderState.clear(); expect(orderState.state.items).toEqual({ items: [] });});
Or you can test it in place by injecting it into a component tree with the inject prop:
test('clear rendered Orders', () => {// render an Orders component with the same orderState as above// (here with react-test-renderer) const tree = TestRenderer.create( <Provider inject={[orderState]}> <Orders /> </Provider>, );// make sure our order rendered (1 item) expect(tree.root.findAllByType(OrderItem).length).toBe(1);// "press" the leave-table button by calling its onPress, which will// call our `clear()` method const leaveTableButton = tree.root.findByProps({ testId: 'leave-table-button', }); button.props.onPress(); expect(tree.root.findAllByType(OrderItem).length).toBe(0);});
Follow Your Bliss
In fact, Unstated is so easy to use, it would be easy to use it for everything, but thatâs not really the intent of the library. Use it judiciously, though, and it will fill in a small but very important hole in your state management. Using Unstated in my current project feels like Iâve finally arrived at a complete data solution, and Iâm no longer wishing for something different.
Originally published at www.okgrow.com.
State of Bliss: Handle Your State with React, Apollo, and Unstated was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.