DEV Community

Suhan Wijaya
Suhan Wijaya

Posted on • Edited on • Originally published at Medium

1 1

Decouple Data from UI with React Hooks

And how I “program to an interface” with JavaScript functions

Source: Imgur


I‘m certain you have seen (or written) this common React pattern: (a) render a placeholder/ loader/spinner while some data is fetched via AJAX, then (b) re-render the component based on the data received. Let’s write a functional component leveraging the Fetch API to accomplish this.

import React, { useState, useEffect } from 'react';
const SomeComponent = (props) => {
const [someData, setSomeData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/some-data')
.then(response => response.json())
.then(data => setSomeData(data))
.catch(error => setError(error))
.finally(() => setLoading(false));
}, []);
return (
<React.Fragment>
{loading && <div>{'Loading...'}</div>}
{!loading && error && <div>{`Error: ${error}`}</div>}
{!loading && !error && someData && <div>{/* INSERT SOME AMAZING UI */}</div>}
</React.Fragment>
);
};

Let’s say my app grows, and there are X components that use the same data fetching logic because… reasons. To avoid spamming the server with data requests, I decide to use Local Storage to cache the data.

OK… does that mean I need to update the data logic X times? 😬😱

Nope, let’s DRY it up by writing a custom hook useSomeData.

import React, { useState, useEffect } from 'react';
const useSomeData = () => {
const cachedData = JSON.parse(localStorage.getItem('someData'));
const [someData, setSomeData] = useState(cachedData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!someData) {
setLoading(true);
fetch('/some-data')
.then(response => response.json())
.then(data => {
localStorage.setItem('someData', JSON.stringify(data));
return setSomeData(data);
})
.catch(error => setError(error))
.finally(() => setLoading(false));
}
}, []);
return { someData, loading, error };
};

The components that share this data logic now look concise.

const SomeComponent = (props) => {
const { someData, loading, error } = useSomeData();
return (
<React.Fragment>
{loading && <div>{'Loading...'}</div>}
{!loading && error && <div>{`Error: ${error}`}</div>}
{!loading && !error && someData && <div>{/* INSERT SOME AMAZING UI */}</div>}
</React.Fragment>
);
};
const AnotherComponent = (props) => {
const { someData, loading, error } = useSomeData();
return (
<React.Fragment>
{loading && <div>{'Loading...'}</div>}
{!loading && error && <div>{`Error: ${error}`}</div>}
{!loading && !error && someData && <div>{/* INSERT ANOTHER AMAZING UI */}</div>}
</React.Fragment>
);
};

OK… DRY code is great, but so what?

Let’s say my app becomes complex, so I decide to use Redux to handle AJAX requests and maintain a global app state. I simply update the implementation of useSomeData without touching the UI components.

import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectSomeData } from 'path/to/data/selectors';
import { fetchSomeData } from 'path/to/data/action';
const useSomeData = () => {
const dispatch = useDispatch();
const someData = useSelector(selectSomeData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!someData) {
setLoading(true);
dispatch(fetchSomeData())
.catch(error => setError(error))
.finally(() => setLoading(false));
}
}, []);
return { someData, loading, error };
};

Then GraphQL comes along and I jump on the bandwagon. Again, I simply update the implementation of useSomeData without touching the UI components.

import { gql, useQuery } from '@apollo/client';
const useSomeData = () => {
const { data: someData, loading, error } = useQuery(gql`
fetchSomeData {
data {
# some fields
}
}
`);
return { someData, loading, error };
};

Rinse and repeat whenever I’m compelled to update the data layer with the latest/hottest state management framework or API paradigm.

To me, this looks a lot like the classic Dependency Inversion Principle, the “D” in SOLID (check out this excellent explainer by Matthew Lucas). While this is not OOP by any means, where we formally define an abstract Interface and create a concrete Class that implements that Interface, I would argue that there is a de facto “interface” that useSomeData provides to the various UI components using it. In this example, the UI doesn’t care how useSomeData works, as long as it receives someData, loading, and error from the hook.

So in theory, this frees the UI from being locked into any particular implementation of the data layer, and enables migrating to new implementations (frameworks/libraries/etc) without having to update the UI code, as long as the “interface” contract is honored.

Curious to hear your thoughts.

Source: Pinterest


P.S. The Container pattern, Render Props, and HOC are popular options to decouple the data layer from the UI layer for classical components. This article is not meant to be a debate as to whether Hooks is better or worse. I’m simply sharing how I learned to use Hooks to apply the same separation of concerns.


Read More


📫 Let’s connect on LinkedIn or Twitter!

Tiugo image

Modular, Fast, and Built for Developers

CKEditor 5 gives you full control over your editing experience. A modular architecture means you get high performance, fewer re-renders and a setup that scales with your needs.

Start now

Top comments (0)

Neon image

Next.js applications: Set up a Neon project in seconds

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Get started →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay