Skip to content

Instantly share code, notes, and snippets.

@rockchalkwushock
Last active December 1, 2021 17:49
Show Gist options
  • Save rockchalkwushock/eddafbe866ea0befe2705cd6216f6714 to your computer and use it in GitHub Desktop.
Save rockchalkwushock/eddafbe866ea0befe2705cd6216f6714 to your computer and use it in GitHub Desktop.
Contra code challenge
import * as React from "react";
interface Props {
fallback: React.ReactElement;
}
interface State {
error: null | string;
hasError: boolean;
}
class ErrorBoundary extends React.Component<Props, State> {
state = { error: null, hasError: false };
static getDerivedStateFromError(error) {
return {
hasError: true,
error
};
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
export default ErrorBoundary;
import axios from 'axios'
type Status = 'error' | 'pending' | 'success'
export const fetchUserProfileById = (id: number) => {
// Start in a pending or loadign state
let status: Status = 'pending'
let result
// Axios returns as a promise so capture the promise for use in the
// 'pending' state below
let suspender = axios(`https://some-api.com/users/${id}`).then(
(r) => {
// if the promise resolves set the data on 'result' and the correct 'status'
result = r.data;
status = 'success';
}
(e) => {
// if the promise rejects set the errro on 'result' and the correct 'status'
result = e;
status = 'error';
}
)
// The code above is asynchronous so it will evaluate when it evaluates we are not
// waiting on it. The results of the async call will be seen when we call the below
// read/0 function that will return based on the current 'status' of the request.
return {
read() {
if (status === 'pending') {
throw suspender
} else if (status === 'error') {
throw result
} else if (status === 'success') {
return result
}
}
}
}
import { Suspense, useState, useEffect } from 'react';
// Problem #1
// We are not using React.lazy
// React.lazy allows for us to lazily load in a component (i.e. code splitting).
// This feature coupled with Suspense allows us to not load in what could possibly be
// a very heavy component into the UI/UX until the data fetching has resolved and if
// it does not resolve we never burdened the user's experience with that heavy component
// that never rendered
const SuspensefulUserProfile = ({ userId }) => {
const [data, setData] = useState({});
useEffect(() => {
fetchUserProfile(userId).then((profile) => setData(profile));
}, [userId, setData])
return (
// Problem #2
// Is that we are not utilizing the "fallback" prop which is required.
// "fallback" is where we can provide a component that will render while the data
// fetching is occuring. This presents the user with some kind of UI/UX to let them
// know we are working on getting them their data instead of a dreaded blank screen.
// Problem #3
// In the same respect the lack of an ErrorBoundary is a problem. Suspense will render
// our fallback component as data fetching is occuring (i.e. loading state), but it will
// continue to show that UI/UX even if the data fetching has failed (i.e. error state).
// This will leave the user wating and waiting for a request to finish that actually has
// we just aren't acting on it.
// Adding an ErrorBoundary will allow us to have a secondary fallback that is specifically
// for the error state.
<Suspense>
// Problem #4
// The asynchronous data fetching is happening outside of the scope of Suspense, so technically
// there is no reason to be using Suspense here at all. The asynchronous data fetching needs to happen
// in the child, or at the very least as per my code example, having the results of that asynchronous call
// being made in the child. This way Suspense will handle the rendering based on the state of the data fetching.
<UserProfile data={data} />
</Suspense>
);
};
const UserProfile = ({ data }) => {
return (
<>
<h1>{data.name}</h1>
<h2>{data.email}</h2>
</>
);
};
const UserProfileList = () => (
<>
<SuspensefulUserProfile userId={1} />
<SuspensefulUserProfile userId={2} />
<SuspensefulUserProfile userId={3} />
</>
);
import * as React from 'react'
import { ErrorBoundary } from '@components/ErrorBoundary'
import { ErrorUI } from '@components/ErrorUI'
import { LoadingUI } from '@components/LoadingUI'
imoprt { fetchUserProfileById } from '@lib/fetchUserProfileById'
interface Props {
userId: string
}
const LazyUserProfile = React.lazy(() => import('@components/UserProfile'))
export const SuspensefulUserProfile: React.FC<Props> = ({ userId }) => {
// When we make this initial call the asynchronous code is executing
// but will not be read until we call the read/0 function.
const resource = fetchUserProfileById(userId)
return (
<ErrorBoundary fallback={<ErrorUI message="Please try back later." />}>
<React.Suspense fallback={<LoadingUI message="Loading..." />}>
<LazyUserProfile resource={resource} />
</React.Suspense>
</ErrorBoundary>
)
}
import * as React from 'react'
import { User } from '@types'
interface Props {
resource: { read(): Pick<User, 'email', 'name'> }
}
const UserProfile: React.FC<Props> = ({ resource }) => {
// We send the resource down into the UserProfile to be read because
// we want Suspense to do our data rendering. If in the event of the
// asynchronous call taking a long time the fallback for Suspense will
// render and should the request fail the ErrorBoundary will run.
const user = resource.read()
return (
<>
<h1>{user.name}</h1>
<h2>{user.email}</h2>
</>
)
}
import * as React from 'react'
import { User } from '@types'
interface Props {
users: Array<Pick<User, 'id'>>
}
export const UserProfileList: React.FC<Props> = ({ users }) => {
return (
<>
{users.map(({ id }) => <SuspensefulUserProfile userId={id} />)}
</>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment