This is the second take home challenge given out by Lambda's engineering team to Lambda X students (students who have completed the program and are looking to land their first job in the industry).
My main goal of this project is to continue expanding my test capabilities with React Testing Library and Jest.
Your challenge is to build a read-only clone of the hacker news frontend using that API.
🖥 Live Project 🖥
-
Caching
ID Lists
and allStory Data
on the initial load- Even though I was only encorporating the
TopStories
in version0.1.0
the initial loadtime to fetch and loop through all the IDs and get their the individual data was>30 seconds
.
- Even though I was only encorporating the
-
Caching
ID Lists
and having the individual<StoryCard />
's fetch their own data- By offloading the fetching to individual
<StoryCard />
's the initial loadtime is down to<1-2 seconds
. - The downside is that there is a short flicker on the individual
<StoryCard />
's while they fetch their own data when a user hits a new page or selects to view more posts on an individual page.
- By offloading the fetching to individual
0.3.0 - Testing w/ React Testing Library & Mock Service Worker
-
I initially started with jest-fetch-mock in order to fake HTTP requests & responses. This worked well with individual components but when it got to the higher level
<App />
component I was struggling to interpret the error messages I was getting. -
After doing more research I came across this article from the esteemed Kent C. Dodds: Stop Mocking Fetch. Ironically this came on the same day that I heard Elon Musk mention that one of the biggest mistakes he sees smart engineers make is 'optomizing something that should not exist'. This naturally led me to asking,
well Mr. Dodds if mock fetching should not exist ... what should I do?!
His answer: Mock Service Worker.
-
Mocking response from the HackerNews API
item
endpoint => returning OBJECT.// Get Item - OBJECT rest.get(`${endpoints.HN_BASE_URL}${endpoints.item}:id.json`, (req, res, ctx) => { return res(ctx.status(200), ctx.json( { title: 'Article_TITLE', by: 'Article_AUTHOR', url: 'https://article_url.com/', score: 100, time: 1600694957, descendants: 43, } )) }),
-
Mocking response from each HackerNews API
IDs
endpoint => each is returning an ARRAY of DIFFERENT lengths. The different lengths are used to test if the<NavBar />
component is correctly routing and rendering the appropriate number of cards.// Get IDs - ARRAY // - 1 - // Top Stories rest.get(`${endpoints.HN_BASE_URL}${endpoints.topStories}`, (req, res, ctx) => { return res(ctx.status(200), ctx.json([1,2,3,4,5])) }), // ...
-
In order to make sure that an error on my end does not let any API fetch calls go out to an actual server I added a
Fallback Handler
in the test server.// Fallback Handler rest.get('*', (req, res, ctx) => { console.error(`Please add request handler for ${req.url.toString()}`) return res( ctx.status(500), ctx.json({error: "Pleade add request handler"}) ) })
<NavBar />
- Testing all
href
attributes.
- Testing all
<StoryCard />
- Happy Path: ✅ - All data appropriatly rendered.
- Missing Data: ✅ - Appropriate conditional rendering.
<DynamicView />
- Passing an
IDs Array
and testing if the correct number ofloading
&rendered
cards are rendered.
- Passing an
<App />
- Having mocked all the return arrays in the
MSW test server
we are selecting each<NavLink />
from the<NavBar />
and testing to make sure the correct number of<StoryCard />'s
are rendered for each route.
- Having mocked all the return arrays in the
0.2.0 - Approach 2 - DynamicViews & Top Stories / New Stories / Ask Stories / Show Stories / Job Stories
-
FETCH_data()
=> a dynamic function used in theuseEffect
of all<StoryCard />
components to get their individual data. It accepts aURL_base
,URL_endpoint
,unique_ID
,URL_suffix
. -
FETCH_ALL_IDs()
=> a dynamic function that is used in the<App />
component'suseEffect
to get all of the needed ID lists. It accepts aURL_base
and anendpoints
array. The function then maps through all the endpoints and returns an array of promises to be utilized byPromise.all()
.
-
<DynamicView />
- This component has replaced all unique view components. It is called in the main
<App />
component'sSwitch Router
and is recieving the appropriate ID List throughRender Props
.
- This component has replaced all unique view components. It is called in the main
-
<CardCreator />
- This is a bridge component that is used to map over a list of IDs and render the appropriate component type that is passed through on props.
-
<StoryCard />
useEffect()
is using theFETCH_data()
util function to get the unique data. This data is then set on the individual component'suseState
hook.
-
<App />
useEffect()
is using theFETCH_ALL_IDs()
util function to get all of the ID lists. This data is then set on theGlobalContext
object.
0.1.0 - Approach 1 - Context API Setup & Homepage Component
-
Provider()
function- Importing the main
GlobalContext
and wrapping itschildren
in theGlobalContext.Provider
.
- Importing the main
-
useReducer()
hook- the
Provider
is importing theinitialState
, anactions
object, and areducer
function. - the
GlobalContext.Provider
is accepting alluseReducer
functinality through the passedvalue
prop.
// Context > Provider.js // IMPORTS // useReducer import initialState from '../useReducer/initialState.js' import actions from '../useReducer/actions.js' import reducer from '../useReducer/reducer.js' // CONTEXT import GlobalContext from '../Context/GlobalContext.js' // EXPORT export default function({children}) { const [ state, dispatch ] = useReducer(reducer, initialState) const value = { topStory_IDs: state.topStory_IDs, setTopStory_IDs: storyIDs => { dispatch({ type: actions.setTopStory_IDs, value: storyIDs }) }, storyData: state.storyData, setStoryData: storyData => { dispatch({ type: actions.setStoryData, value: storyData }) }, } return ( <GlobalContext.Provider value={value}> {children} </GlobalContext.Provider> ) }
- the
-
src > index.js
render- importing the
Provider()
as<ContextProvider />
fromContext > Provider.js
and rendering it through the mainReactDOM.render()
in order to provide theGlobalContext
to whole SPA.
// src > index.js ReactDOM.render( <React.StrictMode> <Router> <ContextProvider> <App /> </ContextProvider> </Router> </React.StrictMode>, document.getElementById('root') );
- importing the
<App />
- Once the
<App />
loads it:- Fetches the
topStories
=> recieves array ofitemIDs
- Updates the
topStory_IDs
on theGlobalContext
through asetTopStory_IDs
function that dispatches an update action
- Updates the
- Loops through the
itemIDs
and gets the individual story details-
story details are added to a prep object object based on the
itemID
:prepObject = { itemID: storyData }
- This is done that that individual story details can be recieved in
O(1)
time when needed.
- This is done that that individual story details can be recieved in
-
- Updates the
storyData
on theGlobalContext
with the prep object through asetStoryData
function that dispatches an update action. - Upates the
<App />
loading state tofalse
and the main SPA router is hit, rendering the<Homepage />
- Fetches the
- Once the
2 <Homepage />
- Uses useContext
and the GlobalContext
to recieve the topStory_IDs
array & the storyData
object
- After accounting for pagination the currentPosts
are mapped and the indvidual story data is recieved in O(1)
time from the storyData
object and passed to <StoryCard />
to render the individual details
0.0.1 - Create React App
- Cleaned out template CRA
<App/>
rendering 'Hello World'
name | source |
---|---|
Y Combinator Logo | seeklogo.com |