How to Fetch Dribbble Shots Using React Hooks
When I was designing my portfolio site, I wanted to import all the work I had published on Dribbble, so that I could have everything in one place.
I could have manually added the designs on the website, but I wanted to have an automated solution.
So that’s exactly what you’ll learn in this article.
How I used the Dribbble V2 API, Axios and React Hooks to import all my work from Dribbble into my portfolio static site made with Gatsby.js.
Registering a New Dribbble App
Before you can start making requests from the Dribbble V2 API, you need to register your app.
For that, you will need to follow these steps —I’m assuming you already have a Dribbble account.
- Open up your Dribbble account settings page
- Click on Applications
- Under Developers, click on Register a new application
Now you should be able to add your own information, similar to how I did it in this example:
The most important parts here are:
- The Callback URL. Is your sites’ URL which you should type including the
https://
part, otherwise you’ll get an error. - The Client ID and Client Secret. You’ll need them to be able to make a POST request, in order to get an access token.
Getting a Dribbble Access Code
The next thing you’ll need is a CLIENT_ID
. To get yours, open a new tab in
your browser and enter the following URL:
https://dribbble.com/oauth/authorize?client_id=CLIENT_ID
CLIENT_ID
is the id from the previous step.
For the app I’m using in this tutorial it’s:
https://dribbble.com/oauth/authorize?client_id=e9e05f3...
Click on Authorize to authorize your new app.
Once you’ve authorized it, you will be redirected to a page with an URL similar to this one:
http://callback_url?code=5asdas892aas8dh8as9d9ashd
The callback_url
is the URL you’ve entered in your Dribbble admin panel. The
important part here is to copy the long code 5asdas892aas8dh8as9d9ashd
, which
you’ll need for the next step.
In order to get the access token, I used Postman —if you need to install it you can follow this link— so that I can make a POST request to the following Dribbble URL:
https://dribbble.com/oauth/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&code=URL_CODE
This will give you the access token you need to be able to make requests to Dribbbles’s V2 API.
The most important parts here are:
- CLIENT_ID: is the client id from the Dribbble account page (look at the first screenshot in this tutorial).
- CLIENT_SECRET: is the client secret from the Dribbble account page (look at the first screenshot).
- URL_CODE: is the code you get after you visit the URL with the
callback_url
of your site.
Making a POST Request With Postman
In order to be able to make a POST request with Postman, you need to follow these steps:
- Open up Postman
- Make a new
POST
Request - Use the URL from the previous step with your
CLIENT_ID
,CLIENT_SECRET
andURL_CODE
Now you should receive a JSON response similar to this one:
{
"access_token": "d8ashd98ashd9a8hsd9ah98dsadsh98ahsd9a8sda",
"token_type": "bearer",
"scope": "public",
"created_at": 1231233453
}
If it doesn’t work, and instead you receive this response —or any response containing an error:
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."
}
You should start over and get a new code.
When I was trying to register mine, I wasn’t able to get the access token and I kept getting this error. After searching for a solution, I found this help page from the Dribbble API docs in which they explain what you can do in case you get an error response from the API.
If everything went fine, you should now have your access token which you can use to send requests to Dribbble’s V2 API 🎉.
Making Async Requests With React Hooks
In order to be able to send requests to the API, I decided to use React Hooks. If you don’t want to use hooks, you can read this article in which you can learn how to send requests to Dribbble’s API using ajax.
Using React Hooks to fetch data from an API is pretty easy. But you should learn how they work first.
It’s really easy to enter an infinite loop of data fetching that will reach the API’s limit in a second.
You should be particularly careful if your API is a paid service like Firebase or AWS.
But don’t worry, I’ll explain how you can avoid running into that later on.
In order to get started you will need the following:
- React v16.8 or newer installed (so that you can use hooks).
- Axios for making requests to Dribbble’s V2 API.
- The Dribbble access token you got from the the previous steps in this tutorial.
Before you continue, I recommend you to read the guide on how to use hooks from Dan Abramov, and this article from Robin Wieruch in which he explains some of the techniques I used in this tutorial, among others.
Although mine is based on Robin’s article, I did change it quite a bit, so that I can render placeholder elements, besides spinners as loading indicators, while the data is being fetched.
But first, let’s start with fetching the shots from Dribbble.
The Dribbble V2 API Endpoint
In order to fetch your own shots from Dribbble, you will need to access the following endpoint:
https://api.dribbble.com/v2/user/shots?access_token=ACCESS_TOKEN&page=1&per_page=4
Where:
ACCESS_TOKEN
: is the token you got after following the first steps in this article.&page=1
: this part isn’t really necessary, I’m using for pagination, more on that later.&per_page=4
: same thing, not necessary, I’m only using it for pagination, keep reading or skip it if you don’t need it.
Fetching Data With Axios
For this tutorial I used axios, which you can install with npm install axios
.
const [isLoading, setIsLoading] = React.useState(true);
const [isError, setIsError] = React.useState(false);
const [dribbblePage, setDribbblePage] = React.useState(1);
const [dribbblePosts, setDribbblePosts] = React.useState([]);
React.useEffect(() => {
async function getDribbblePosts() {
try {
const dribbbleRes = await axios.get( `https://api.dribbble.com/v2/user/shots?access_token=${GATSBY_DRIBBBLE_TOKEN}&page=${dribbblePage}&per_page=${SHOTS_PER_PAGE}`, );
setDribbblePosts([...dribbblePosts, ...dribbbleRes.data]);
if (isLoading) setIsLoading(false);
} catch (error) {
console.warn(error);
if (isLoading) setIsLoading(false);
setIsError(true);
}
}
getDribbblePosts();
}, [dribbblePosts, dribbblePage, isLoading, isError]);
So let’s see what this gets us so far:
- First, I use
async await
to fetch the data, which is stored using thesetState
hook in thedribbblePosts
array. - The
isLoading
state variable is used to render a loading indicator, spinner in most cases, while the data is being fetched. It’s initially set totrue
, and then set tofalse
once we get a response fromawait
. - The entire request is wrapped inside a
Try...Catch
block to handle any errors that might be returned.
You can see I’m also destructuring the previous state in the dribbblePosts
array, because setState()
doesn’t merge the previous state values as it
happens when using classes.
I’m destructuring the previous values plus the new ones so that I can merge the posts from the previous requests with the new data from the incoming requests.
When a user clicks on the Load More button to fetch more posts, a new network request is made, which will fetch more shots.
So far, so good.
But if you try to run the code it will enter an infinite loop and the Dribbble
API will give you a 429 Too Many Requests
error.
You’ll also get a memory leak if you navigate to another page from where the component is rendered before the state is set.
Since the data is being fetched asynchronously using async await
if you
navigate to a different page before it resolves, React will try to save the data
in a state variable of a component that has been unmounted and it will throw
this error:
index.js:2177 Warning: Can’t perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
So let’s see how to fix these issues.
Avoid Making Requests on Each Render
Basically, the loop happens because each time state
is changed in React, it
triggers a new render. Then on each render, a new request will be made, which
changes state
again, which causes a new render, and so on...
One solution would be to simply add an empty array as a dependency of the
useEffect()
hook, so that it only runs on the first render. However, in my
case it wouldn’t work, because I’m changing the state again with:
if (isLoading) setIsLoading(false);
const [isLoading, setIsLoading] = React.useState(true);
const [isError, setIsError] = React.useState(false);
const [dribbblePage, setDribbblePage] = React.useState(1);
const [dribbblePosts, setDribbblePosts] = React.useState([]);
React.useEffect(() => {
async function getDribbblePosts() {
try {
const dribbbleRes = await axios.get( `https://api.dribbble.com/v2/user/shots?access_token=${GATSBY_DRIBBBLE_TOKEN}&page=${dribbblePage}&per_page=${SHOTS_PER_PAGE}`,
);
setDribbblePosts([...dribbblePosts, ...dribbbleRes.data]);
if (isLoading) setIsLoading(false); } catch (error) {
console.warn(error);
if (isLoading) setIsLoading(false); setIsError(true);
}
}
getDribbblePosts();
}, []);
This happens because the first time the component renders, the request will be
made and the response from the API will be stored in the dribbblePosts
array,
which will trigger a render when state is changed.
Then on the second render, the dribbblePosts
will have the data stored inside,
but the API call will run again since there’s nothing preventing it from
executing.
Then the response is stored again in state, which triggers a new render.
The same thing will happen once setIsLoading(false)
is executed. State will
change again, a new render will be triggered, and a new API call will be made.
And you’ve probably noticed that this is the perfect recipe for an infinite
loop, which will give you a 429 Too Many Calls
error.
In my case the solution was pretty simple, I just wrapped axios.get()
with an
if statement.
const [isLoading, setIsLoading] = React.useState(true);
const [isError, setIsError] = React.useState(false);
const [postsFetched, setPostsFetched] = React.useState(false);const [dribbblePage, setDribbblePage] = React.useState(1);
const [dribbblePosts, setDribbblePosts] = React.useState([]);
React.useEffect(() => {
async function getDribbblePosts() {
try {
if (!postsFetched) { const dribbbleRes = await axios.get(
`https://api.dribbble.com/v2/user/shots?access_token=${GATSBY_DRIBBBLE_TOKEN}&page=${dribbblePage}&per_page=${SHOTS_PER_PAGE}`,
);
setPostsFetched(true); }
setDribbblePosts([...dribbblePosts, ...dribbbleRes.data]);
if (isLoading) setIsLoading(false);
} catch (error) {
console.warn(error);
setPostsFetched(true); if (isLoading) setIsLoading(false);
setIsError(true);
}
}
if (!postsFetched) {
getDribbblePosts(); }
}, [dribbblePosts, dribbblePage, postsFetched, isLoading, isError]);
By using the boolean postsFetched
, I can limit the number of network requests
made on each render. The first time the component renders, the network request
will be made, then, since I’m changing state, a new render will be triggered by
React.
However, this time around the network request won’t be made since postsFetched
is true
, and it won’t reach the if statement.
When you are making network requests using useEffect()
hooks, each time the
state is changed, React will render the component. If you don’t check if you
already made a request or if the data is already fetched, it can lead to an
infinite loop of renders and network requests.
Avoiding Memory Leaks
In order to fix this issue, I used the cleanup function that the React hooks provide, which is executed when the component is unmounted.
const [isLoading, setIsLoading] = React.useState(true);
const [isError, setIsError] = React.useState(false);
const [postsFetched, setPostsFetched] = React.useState(false);
const [dribbblePage, setDribbblePage] = React.useState(1);
const [dribbblePosts, setDribbblePosts] = React.useState([]);
React.useEffect(() => {
let didCancel = false; let dribbbleRes = {};
async function getDribbblePosts() {
try {
if (!postsFetched) {
dribbbleRes = await axios.get(
`https://api.dribbble.com/v2/user/shots?access_token=${GATSBY_DRIBBBLE_TOKEN}&page=${dribbblePage}&per_page=${SHOTS_PER_PAGE}`,
);
setPostsFetched(true);
}
if (!didCancel) {
setDribbblePosts([...dribbblePosts, ...dribbbleRes.data]);
if (isLoading) setIsLoading(false);
}
} catch (error) {
console.warn(error);
if (!didCancel) { if (isLoading) setIsLoading(false);
setPostsFetched(true);
setIsError(true);
}
}
}
if (!postsFetched && !didCancel) { getDribbblePosts();
}
return () => {
didCancel = true; };
}, [dribbblePosts, postsFetched, dribbblePage, isLoading, isError]);
By using a boolean didCancel
I can avoid saving data in state if the component
is unmounted. The cleanup function can be used to toggle the boolean from
false
to true
. Then the next time it’s executed, it won’t save anything in
state. This way React won’t try to save data in the state of a component that
has been unmounted.
Great, so far we have a useEffect()
hook that:
- Only fetches data from the Dribbble API once
- Stores the data only while the component is still mounted
With just these optimizations you could already call it a day.
But I wanted to display placeholder elements while the data is being fetched, to avoid large layout shifts when the posts are rendered on the screen.
Rendering Placeholder Elements
In order to render the placeholder, besides the loading indicator, I used the Dribbble API’s pagination features with a certain amount of shots per request.
You probably have seen I was using template literal strings to create the URL:
await axios.get(
`https://api.dribbble.com/v2/user/shots?access_token=${GATSBY_DRIBBBLE_TOKEN}&page=${dribbblePage}&per_page=${SHOTS_PER_PAGE}`,
);
The Dribbble V2 API gives you the ability to use pagination with the &page=X
and &per_page=X
query parameters.
Creating the Placeholder Elements
On the initial load, I wanted to render only 4 or 6 shots. To do that, I’m using a constant that stores the amount of shots I want to fetch.
const SHOTS_PER_PAGE = 4;
This was then used in the axios.get()
URL to fetch 4 shots on each paginated
page and to create the placeholder elements. The placeholders are made by
creating an array that had as many values inside, as the length of the
SHOTS_PER_PAGE
const.
const placeholderArr = Array.from({ length: SHOTS_PER_PAGE }, (v, i) => i);
When using Array.from()
you can specify a second argument which is a map()
function, that will run on each of the elements in the array. This way I can
create a pre-filled array which I can use to map over while isLoading=true
,
and render placeholder elements.
{
isLoading && placeholderArr.map((i) => <DribbblePostPlaceholder key={i} />);
}
To create these elements you can use the following code. It’s just a wrapper div
with position: relative;
and a child element with a padding-bottom: 75%;
.
The padding-bottom of 75% is used to have the same aspect ratio as the images
fetched from Dribbble.
I’m also using a background-position
animation to animate the background from
a light gray to a darker one, to simulate a loading indicator.
Note that I’m using Styled Components in this tutorial.
const StyledPlaceholder = styled.div`
position: relative;
overflow: hidden;
height: 100%;
width: 100%;
`;
const placeholderAnimation = keyframes`
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
`;
const StyledPlaceholderInner = styled.span`
display: block;
width: 100%;
padding-bottom: 75%;
background: linear-gradient(
90deg,
${theme.colors.gray100},
${theme.colors.gray400},
${theme.colors.gray100}
);
background-size: 200% 200%;
animation: ${placeholderAnimation} 3s ease infinite;
`;
function DribbblePostPlaceholder() {
return (
<StyledPlaceholder>
<StyledPlaceholderInner />
</StyledPlaceholder>
);
}
Once isLoading
is changed to false
, the placeholder elements are replaced
with the shots from Dribbble.
{
!isLoading &&
dribbblePosts.map((post) => <DribbblePost key={post.id} post={post} />);
}
Loading More Shots
When I was designing the page where the component is mounted, I only wanted to show 4 or 6 shots at most.
But I also wanted to have an option to load more shots if any user wanted to see more of them, without having to redirect them to the Dribbble homepage.
Initially, I thought of fetching several posts, 20 or so, and slicing the array into 4 to 6 long chunks so that I only rendered a couple of shots at a time. But I soon realized that it wasn’t a really good idea to download that many images. This was especially important for the users that are visiting my site on a mobile connection with limited bandwidth.
So I decided to only load 4 shots initially, and if any user wanted to see more, they could use the Load More button which would fetch 4 shots more.
Since the Dribbble API has built-in pagination which lets me load a certain amount of shots per page, it was the perfect solution to my problem.
In order to load more shots, I added a button that, when clicked, would trigger
the loadMorePosts()
function.
<LoadMore onClick={loadMorePosts}>
{!isLoadingMore && <LoadMoreLabel>Load More...</LoadMoreLabel>}
{isLoadingMore && <Spinner />}
</LoadMore>
Once the button is pressed, the function will add 1 to the dribbblePage
state
variable and set the postsFetched
boolean used to control whether the
axios.get()
method is executed.
function loadMorePosts() {
setDribbblePage(dribbblePage + 1);
setPostsFetched(false);
setIsLoadingMore(true);
}
In addition, the isLoadingMore
variable lets me render more placeholder
elements, while the new shots are fetched from the Dribbble API.
By using a different variable and not the original isLoading
one, I could
avoid changing the shots I already had and rendered, thus adding new
placeholders and shots below the ones I already had.
In order to make this all work, I also needed to merge the previous state with
the new state, since useState()
overrides all existing values in the state.
{
isLoadingMore &&
placeholderArr.map((i) => <DribbblePostPlaceholder key={i} />);
}
The Final Version of the Dribbble Posts Component
function DribbblePosts() {
const SHOTS_PER_PAGE = 4;
const [isLoading, setIsLoading] = React.useState(true);
const [isLoadingMore, setIsLoadingMore] = React.useState(false);
const [isError, setIsError] = React.useState(false);
const [postsFetched, setPostsFetched] = React.useState(false);
const [dribbblePage, setDribbblePage] = React.useState(1);
const [dribbblePosts, setDribbblePosts] = React.useState([]);
const placeholderArr = Array.from({ length: SHOTS_PER_PAGE }, (v, i) => i);
React.useEffect(() => {
let didCancel = false;
let dribbbleRes = {};
async function getDribbblePosts() {
try {
if (!postsFetched) {
dribbbleRes = await axios.get(
`https://api.dribbble.com/v2/user/shots?access_token=${GATSBY_DRIBBBLE_TOKEN}&page=${dribbblePage}&per_page=${SHOTS_PER_PAGE}`,
);
setPostsFetched(true);
}
if (!didCancel) {
setDribbblePosts([...dribbblePosts, ...dribbbleRes.data]);
if (isLoading) setIsLoading(false);
if (isLoadingMore) setIsLoadingMore(false);
}
} catch (error) {
console.warn(error);
if (!didCancel) {
if (isLoading) setIsLoading(false);
if (isLoadingMore) setIsLoadingMore(false);
setPostsFetched(true);
setIsError(true);
}
}
}
if (!postsFetched && !didCancel) {
getDribbblePosts();
}
return () => {
didCancel = true;
};
}, [
dribbblePosts,
postsFetched,
dribbblePage,
isLoading,
isError,
isLoadingMore,
]);
function loadMorePosts() {
setDribbblePage(dribbblePage + 1);
setPostsFetched(false);
setIsLoadingMore(true);
}
return (
<DribbblePostsWrapper>
{isError && <ErrorMessage>Sorry, something went wrong...</ErrorMessage>}
{isLoading &&
placeholderArr.map((i) => <DribbblePostPlaceholder key={i} />)}
{!isLoading &&
dribbblePosts.map((post) => <DribbblePost key={post.id} post={post} />)}
{isLoadingMore &&
placeholderArr.map((i) => <DribbblePostPlaceholder key={i} />)}
<LoadMore onClick={loadMorePosts}>
{!isLoadingMore && <LoadMoreLabel>Load More...</LoadMoreLabel>}
{isLoadingMore && <Spinner />}
</LoadMore>
</DribbblePostsWrapper>
);
}
(Update) Refactoring to useReducer
So far, the component is working great.
But it could be better.
The main issue I have with it so far, is that I need to use a lot of if
statements that stop rendering when the state changes. Currently, I have more
than 4 different state changes inside the useEffect
hook.
Also, the hooks’ dependency list is getting pretty long:
React.useEffect(() => {
...
}, [dribbblePosts, postsFetched, dribbblePage, isLoading, isError, isLoadingMore]);
A great solution to this is to use the reducer hook, which lets me combine several state changes in a single call.
Actually not much code needs to change.
The components render function is practically the same.
I only need to extract the data fetching logic in a separate file —so that I can have cleaner code.
And I also need to destructure the variables used in the render function from
the custom hook useDribbbleReducer()
.
function DribbblePosts({ locale }) {
const {
dribbblePosts,
shotsPerPage,
isLoading,
isLoadingMore,
isError,
loadMorePosts,
} = useDribbbleReducer();
const placeholderArr = Array.from({ length: shotsPerPage }, (v, i) => i);
return (
<DribbblePostsWrapper>
<DribbblePostH1>Latest Designs</DribbblePostH1>
<DribbbleSubhead>
Some of the latest projects I shared on Dribbble.
</DribbbleSubhead>
{isError && <ErrorMessage>{DRIBBBLE_STATUS[locale].error}</ErrorMessage>}
{isLoading &&
placeholderArr.map((i) => <DribbblePostPlaceholder key={i} />)}
{!isLoading &&
dribbblePosts.map((post) => <DribbblePost key={post.id} post={post} />)}
{isLoadingMore &&
placeholderArr.map((i) => <DribbblePostPlaceholder key={i} />)}
<StyledLoadMore onClick={loadMorePosts}>
{!isLoading && !isLoadingMore && (
<LoadMoreLabel>Load More...</LoadMoreLabel>
)}
{(isLoading || isLoadingMore) && <Spinner />}
</StyledLoadMore>
</DribbblePostsWrapper>
);
}
Creating the Reducer
const reducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT": {
return {
...state,
};
}
case "FETCH_SUCCESS": {
return {
...state,
isLoading: false,
isLoadingMore: false,
dribbblePosts: [...state.dribbblePosts, ...action.payload], };
}
case "FETCH_MORE": {
return {
...state,
isLoadingMore: true,
dribbblePage: state.dribbblePage + 1,
};
}
case "FETCH_ERROR": {
return {
...state,
isLoading: false,
isLoadingMore: false,
isError: true,
};
}
default:
return state;
}
};
The reducer is pretty simple. I can have an initial state with isLoading
,
dribbblePosts
, ..., in a single object that is updated when the reducer
dispaches the actions.
In order to still be able to add new shots to the existing ones, I needed to merge the previous state with the new fetched shots.
Besides that, I also used the axios cancel token (instead of the didCancel
variable) so that I can cancel the network request if the component is unmounted
before the async function resolves.
export default function useDribbbleReducer() {
const initialState = {
dribbblePage: 1,
shotsPerPage: 4,
dribbblePosts: [],
isLoading: true,
isLoadingMore: false,
isError: false,
};
const [state, dispatch] = React.useReducer(reducer, initialState);
React.useEffect(() => {
let dribbbleRes = {};
let source = axios.CancelToken.source();
dispatch({ type: "FETCH_INIT" });
const fetchData = async () => {
try {
dribbbleRes = await axios.get(
`https://api.dribbble.com/v2/user/shots?access_token=${GATSBY_DRIBBBLE_TOKEN}&page=${state.dribbblePage}&per_page=${state.shotsPerPage}`,
{
cancelToken: source.token, },
);
dispatch({ type: "FETCH_SUCCESS", payload: dribbbleRes.data });
} catch (error) {
if (axios.isCancel(error)) { console.warn("Cancelled axios request");
} else {
console.warn(error);
dispatch({ type: "FETCH_ERROR" });
}
}
};
fetchData();
return () => {
// Prevent memory leak when moving to another page and cancel axios request
source.cancel(); };
}, [state.dribbblePage, state.shotsPerPage]);
function loadMorePosts() {
// Load posts with pagination, shotsPerPage on each page
dispatch({ type: "FETCH_MORE" });
}
return { ...state, loadMorePosts };
}
As you can see, the dependency list of the useEffect
hook is now much cleaner.
It only needs to be aware of the changes made to the dribbblePage
variable
which is updated when I want to load more shots from the API, using pagination.
The only check I need to keep doing is the didCancel
one, which is used to
prevent saving data into state if the component unmonunts before the async
function resolves.
Final Thoughts
So far I’m quite happy with the results. I got all the features I needed by combining React Hooks with the the pagination feature of Dribbble’s V2 API.
As an improvement, I’m looking into how I can cache the results.
Currently, each time the component mounts, the shots are fetched from Dribbble. Therefore, each time a user loads the page that has the component, a network request is made.
By caching the shots fetched from Dribbble, I can avoid making a network request each time the component is rendered. This way I can help the visitors of my site to save some of their mobile bandwidth.