10 KiB
Use React Container Component Abstraction
- Status: accepted
- Deciders: Lauren Zugai, Dan Schomburg, Barry Chen
- Date: 2023-07-12
Context and Problem Statement
In #8138, the FxA team proposed and later implemented a refactoring effort to move API logic out of components in favor of an Account
class abstraction that lives on AppContext
and is accessible via a useAccount
hook. While working on converting the remainder of fxa-content-server
from Backbone to React in our fxa-settings
package, extending the Account
model has been cumbersome and Account
has grown to be too large by essentially acting as our global API layer and model. Currently, we must pull from Context
with useAccount
just to make an API call even when account data is not required.
Decision Drivers
- A large query runs on the first
useAccount
call, which we do across our newly converted pages just to make an API call even when data is not required. On non-Settings pages, this causes initial over fetching, some data which is not even available without a session token - We should be mindful of using Context but rely on it often, leading to tedious mocking and other issues like impure functions, possibly unintended rerenders, and less explicit and oftentimes unneeded component dependencies
- Our newly converted pages have varying data requirements and we need a consistent pattern set for when and where to use models and validation
- We currently use Apollo Client's query and mutation methods directly instead of using hooks which is not ideal (see "More Info"), but we want to keep our presentation layer light and don't want to have to use
<MockedProvider>
across all of our tests - More consistent error handling and loading states; we have a mix of error handling in
Account
and our components, andaccount.loading
has not been consistently used or reliable
Considered Options
- Option A, separate
Account
as "authed" and "non-authed" - Option B, per-page model + API layer like
Account
, validate with HOC or container - Option C, per-page container components and models, validate in container
Decision Outcome
Chosen option: Option C, because it most comprehensively resolves the issues and factors driving the decision.
See the prototype for this option that helped informed the decision outcome.
This decision applies to React pages in the fxa-settings
package but not Settings pages. Once we've applied this pattern to newly refactored React pages we can decide how we want to handle Settings (which we may want to use just one container component for).
More Info
It’s common practice to create an instance of Apollo Client at the root of a GraphQL application and pass the instance into ApolloProvider
, allowing the use of Apollo’s hooks like useQuery
and useMutation
anywhere in the component tree.
Currently, we create the Apollo Client instance and instead, place it on our AppContext
, then pull it into our Account model to perform queries and mutations away from our React components. As mentioned, we began using this pattern for better separation of presentation code from data-fetching logic and to make mocking easier — we don’t have to mock GQL queries/mutations or use MockedProvider. Though, it's worth noting we have very minimal tests for our Account
model at present.
If we choose not to perform operations inside React components using ApolloProvider and Apollo provided hooks, we must continue to use apolloClient.query
and apolloClient.mutate
. However, there are a few potential downsides associated with using these methods that are worth considering:
- This approach is uncommon - documentation and examples are much more sparse
- These methods are not tied to the React lifecycle, causing manual management of loading and error states as well as manually handling state updates since it does not automatically cause a rerender
- Polling, which we use to check account and session status on signin, signup, and Sync screens, is slightly more complicated because Apollo Client’s query method doesn’t support polling out of the box, only the hooks do. To reference
apolloClient.query
with polling, we have to usesetInterval
andclearInterval
. (Note: We can’t use Apollo Client subscriptions because our database does not have real-time functionality.)
It's also worth noting that prior to the refactor to use Account
, we had a custom MockedProvider
called MockedCache
to aid with testing boilerplate, and a custom useMutation
hook that reported network errors to Sentry (see docs that explain more).
All proposed options keep GQL in one layer, which will make it easier to refactor away from GQL if we ever desire to in the future compared to abstracting mutations and performing queries in page components, for example, as we would need to use MockedProvider
in almost all of our tests.
Pros and Cons of the Options
Option A, separate Account
as "authed" and "non-authed"
The FxA team held a preliminary meeting about our front-end architecture and initially noted splitting our Account model into an authenticated and non-authenticated version could address at least some concerns because a lot of account data requires a session token to access, and our global useAccount
query attempts to fetch this data regardless of sessionToken status.
This was suggested prior to identifying further problems and decision drivers. This is not an ideal solution because we would still have two large models with overlapping methods that may be difficult to split later and does not address all related concerns, though better inversion of control could help this option be more viable.
Per-page model + API layer like Account
, validate with HOC or container
Description:
- Instead of a single
Account
model that provides an API layer and account data, create one per page. Continue usingapolloClient.query
andapolloClient.mutate
and continue referencingmodel.apiCallHandler
in components - Pass dependencies into each page component rather than store them on Context
- If the page needs any validation, create a separate model containing the data to validate, and create a HOC or container component to perform the validation in (similar to our current
LinkValidator
component)
Pros and cons:
- Good, because it's better inversion of control than what we have now while keeping a similar pattern
- Good, because it allows us to move away from the
useAccount
hook and makes it easier to see what data needs to be fetched and used per page - Bad, because it's preferable to use hooks over
apolloClient
direct methods - Bad, because
apolloClient
andauthClient
should be singletons that we would need to pass into every new page model, or alternatively always pull these out from Context - Bad, because our loading and error states may still be inconsistent without more refactoring
Per-page container components and models, validate in container
See the prototype for this option that helped informed the decision outcome.
Description:
- Every page component that needs an API call or data validation will have a corresponding container component to handle them
- Wrap the application in
<ApolloProvider>
so the container component can execute queries and mutations with hooks - Only perform API calls in container components. This includes
useQuery
for page-specific needed data, which will be stored in and then read from Apollo Cache in future queries,useMutation
mutations, andauthClient
calls (until those can be refactored to use GQL) - Handle all error processing in the container component
- If the page needs any validation, create a separate model containing the data to validate, and perform the validation in the container component
Pros and cons:
- Good, because container components are a common pattern and this approach provides a better separation of concerns among our API/network/business logic layer, data models, and presentation layer
- Good, because of more flexibility and inversion of control. We won’t have to mock entire models or partials with
as unknown as Account
type casting and can instead, mock only the handlers and their results when writing page-level tests - Good, because it allows us to move away from the
useAccount
hook and makes it easier to see what data needs to be fetched and used per page - Good, because placing validation and API handlers in the same location is explicit and clear
- Good, because of benefits gained when using Apollo Client's hooks (see "More Info")
- Good/neutral, because while we can create consistent error handling in container components, the prototype has shown we likely want to manually handle loading states in page components. Good for pattern setting, but not as smooth as being able to use Apollo Client's
result
object witherror
,loading
, anddata
states - Neutral, because GQL related container tests will require a
MockedProvider
. Most tests will be presentation/page-level and will not requireMockedProvider
, but while this is standard when testing Apollo Client and will give us a higher degree of confidence in our API layer, it's another testing tool that engineers will need to familiarize themselves with - Neutral, because we can isolate changes to non-Settings pages first without taking on a massive refactoring effort, but differing patterns between the two may feel awkward