
No Comments Yet
Be the first to share your thoughts and start the conversation.
Be the first to share your thoughts and start the conversation.
Complete source code available till this point of lesson is available at
Provided default UI states for question and other related are here
import ROUTES from "./routes";
export const DEFAULT_EMPTY = {
title: "No Data Found",
message:
"Looks like the database is taking a nap. Wake it up with some new entries.",
button: {
text: "Add Data",
href: ROUTES.HOME,
},
};
export const DEFAULT_ERROR = {
title: "Something Went Wrong",
message: "Even our code can have a bad day. Give it another shot.",
button: {
text: "Retry Request",
href: ROUTES.HOME,
},
};
export const EMPTY_QUESTION = {
title: "Ahh, No Questions Yet!",
message:
"The question board is empty. Maybe it’s waiting for your brilliant question to get things rolling",
button: {
text: "Ask a Question",
href: ROUTES.ASK_QUESTION,
},
};
export const EMPTY_TAGS = {
title: "No Tags Found",
message: "The tag cloud is empty. Add some keywords to make it rain.",
button: {
text: "Create Tag",
href: ROUTES.TAGS,
},
};
export const EMPTY_COLLECTIONS = {
title: "Collections Are Empty",
message:
"Looks like you haven’t created any collections yet. Start curating something extraordinary today",
button: {
text: "Save to Collection",
href: ROUTES.COLLECTION,
},
};
By logging in, you'll unlock full access to this and other free tutorials on JSM Pro.
Why? Logging in lets us personalize your learning experience, track your progress, and keep you in the loop with new workshops, coding tips, and platform updates.
You'll also be the first to know about upcoming launches, events, and exclusive discounts.
No spam—just helpful content to level up your skills.
If that sounds fair, go ahead and log in to continue →
Enter your name and email to get instant access
In this lesson, we explore how to improve the user interface by effectively managing empty states and error states in the display of data. The discussion emphasizes the importance of showing visually appealing messages rather than generic text when there are no questions or an error occurs. By leveraging design prototypes and creating a reusable data renderer component, developers can ensure a consistent and polished UI that enhances user experience.
data-renderer.tsx
, should be implemented to manage displays for different data scenarios.states.ts
file for consistency.00:00:02 While we did a great job fetching all of these questions, or currently just one, and displaying them on the homepage, there is always room for improvement.
00:00:10 Currently, we're just showing the questions, but if there are no questions, we just show an ugly text that says no questions.
00:00:17 And what if there's an error fetching all of these questions?
00:00:20 How do we display that without breaking the rest of the UI?
00:00:23 Well, let's take a quick look at the design.
00:00:25 If you go here and go to the right, you should be able to find two different states.
00:00:31 The no questions to show state, as well as oops something went wrong state, the error state.
00:00:38 These are two special illustrations alongside the text and the button that our designer has created so we can properly convey the errors to the user.
00:00:46 You can see them in the light mode as well.
00:00:48 So let's go ahead and download them together.
00:00:50 I'll first show my UI and then I will select this illustration here and simply export it.
00:00:58 And I'll repeat the same thing with the error illustration.
00:01:02 You have to get into it and then export it as well as the two light ones right here and right here.
00:01:11 Once you have them, go back to your code and head over to public images.
00:01:16 I'll do that by simply drag and dropping them from the downloads.
00:01:19 And we will have to change the naming a bit.
00:01:22 For the error state, let's do that first.
00:01:25 We have the dark error, so let's rename it to dark-error.png.
00:01:30 And we also have a light error, light-error.png.
00:01:36 And when it comes to the illustrations, I think we already have them, so we don't have to get them again.
00:01:41 So I'll simply remove this illustration 1 and illustration 2, as we already have a dark dash illustration and light dash illustration.
00:01:49 So now you know how you can properly download images from the design and put them to your code.
00:01:54 Now, before we code the UI for these two states, empty and error, which look very similar one to another, there is one important thing to note down.
00:02:02 And that's that there will be many places where we'll have to do the same things over and over again.
00:02:09 Map the data, depending on criteria and conditions, such as the questions, and then show that empty state or show the error state.
00:02:18 Same thing happens for the collections.
00:02:20 Map over the collections, choose the criteria, and then either display the questions or show the empty or error states.
00:02:27 Same thing for jobs, tags, communities, questions, and more.
00:02:31 So to avoid being repetitive, let's create a common renderer for this.
00:02:36 This is another great practice that I want to teach you.
00:02:39 Head over into components and create a new file called data-renderer.tsx.
00:02:46 Within it, run RAFCE, and soon we'll be able to start implementing it.
00:02:51 But before we do that, we need to decide what our messages will look like if there's no questions or tags.
00:02:58 So, we can create some templates and then reuse them whenever necessary depending on the type of the data we're trying to render.
00:03:06 To do that, we can head over into constants and create a new file called states.ts.
00:03:12 Here, we can declare a new state, such as export const default underscore empty is equal to an object.
00:03:21 And here we can define how it would look like.
00:03:24 It'll have a title equal to no data found.
00:03:29 Then we can put a message saying something like, looks like the database is taking a nap.
00:03:38 wake it up with some new entries, something like that.
00:03:43 And we can also display some kind of a button that'll have a text equal to add data, as well as an href, which is going to be equal to routes.home.
00:03:55 So it'll actually point to home, and this routes is coming from .slash routes.
00:04:00 And just for a second, let's actually look into the routes to make sure that we have all of the necessary things in there.
00:04:06 We have the home page, the sign in and the sign up, as well as the ask question.
00:04:11 I think we're missing a route that points to the collection route.
00:04:15 So I'll add it.
00:04:15 That's collection pointing to forward slash collection.
00:04:19 And we also have community, which is another one of list items on the left side.
00:04:23 So I'll add community.
00:04:24 After that, we can point to tags, which is going to be equal to forward slash tags.
00:04:30 And we can do the same thing for jobs, pointing to forward slash jobs.
00:04:34 Next, we have the profile details, which is just profile.
00:04:37 Question details, which is just question.
00:04:39 And then this should be tag and not tags because we already have a plural of tags.
00:04:45 And finally, we have sign in with OAuth.
00:04:47 Great.
00:04:48 Let's just see where we used this routes.tags before just so we can modify it.
00:04:53 So that was routes.tags right here.
00:04:57 This will now be routes.tag.
00:05:00 There we go.
00:05:01 Make sure to change that because we changed the name right here.
00:05:04 Now that we have those routes, we know that we have properly created a default empty state.
00:05:09 In a similar way, we can create a default error state by saying export const default error saying, oops, something went wrong.
00:05:19 And we can change the message to say something like, even our code.
00:05:25 can have a bad day.
00:05:27 Give it another shot.
00:05:29 The text will be try again, and it's going to point to routes.home.
00:05:33 Now, these are very specific, because we don't yet know where we want to show them.
00:05:37 They are the defaults.
00:05:38 But we can also have a bit more specific states, such as export const empty question.
00:05:45 So we will display this whenever we're trying to fetch the questions.
00:05:49 And the title here can be a bit more specific.
00:05:52 Ah, no questions yet.
00:05:55 The message can say something like, the question board is empty.
00:06:01 Maybe it's waiting for you to ask something.
00:06:04 You get the idea, and now we can create a couple of these states, which we can use from within our code.
00:06:09 Just so you don't have to manually type it out, I'll provide this file right below this lesson so you can copy it and paste it here.
00:06:16 We already created the default empty, the default error, and the empty question.
00:06:20 And now we also added empty tags, empty collection, and that's about it.
00:06:25 Now let's develop the data renderer component.
00:06:29 I'll make it accept many different kinds of props.
00:06:32 such as success, error, data, and then how the empty state will look like, which will be empty, equal to default underscore empty,
00:06:42 which we can import from constants, as well as what we want to render.
00:06:46 These will be of a type props, which we can define soon, and then we can pass a generic T parameter into it, as well as before we call it,
00:06:55 we can decide that sometimes we can pass some additional T properties right here.
00:07:00 So that'll look something like this.
00:07:02 And of course, we have to define an interface of props right here, which accepts that generic T type.
00:07:09 And this is to make our data type safe, even though we don't yet know how the data we're passing into this data renderer will look like.
00:07:16 It can be questions, it can be tags, or it can be something entirely different, which is why I defined it as a generic T prop.
00:07:23 But what we do know is that a success will be of a type boolean, The error will be optional of a type object that has a message of a type string and details,
00:07:35 which is optional of a type record, string, and an array of strings.
00:07:41 We already defined that this is how errors typically look like.
00:07:45 We'll also have the data.
00:07:47 which will be an array of tees or null or undefined in case we don't get it.
00:07:54 And we can define what an empty state will have.
00:07:57 It'll have a title of string, a message of string, as well as a button, which is optional.
00:08:05 That's going to have a text of string.
00:08:09 as well as an href of string as well.
00:08:12 And finally, we'll have the render method, which will accept the data of a type, an array of Ts, which can be questions,
00:08:19 tags, or something else.
00:08:21 And it'll simply return react.reactNode.
00:08:28 So it'll actually render them.
00:08:31 Now we have an interface of these props and we are ready to try to render the data.
00:08:36 But before we go ahead and render the data, I want to create a new reusable component, which is going to be this component right here,
00:08:43 containing of the image, text, description, and a button.
00:08:48 Because you can see we are reusing it in all different situations.
00:08:52 So let's create a new one called const state skeleton.
00:08:57 is equal to an immediate return arrow function that's going to accept some different props such as image, title, message,
00:09:07 and the button.
00:09:08 Basically everything that is different of a type state skeleton props.
00:09:15 And now we can define an interface of these state skeleton props We already know how it looks like.
00:09:24 It's going to have an image which can have either a light property of a type string, or a dark property of a type string,
00:09:33 and an alt tag of a type string.
00:09:35 Next, we can have a title of a type string, a message of a type string, And finally a button which will be optional that is an object that has a text of
00:09:46 a type string as well as an href of a type string.
00:09:50 And now we can define this reusable component.
00:09:53 Let's make it into a div.
00:09:56 That'll have a class name equal to margin top of 16. And within it, let's display an empty react fragment within which I can render an image that'll look
00:10:08 something like this.
00:10:09 Of course, we have to import this image from next image.
00:10:12 Pass it a source equal to image.dark.
00:10:17 I'll tag of image.alt.
00:10:18 A width of 270, a height of 200. and a class name of hidden object-contain, but only if we're in dark mode, then make the display of block to make it visible.
00:10:39 And now, here we're trying to render the data, but if there is no data, or if data.length is equal to zero, in that case,
00:10:52 we will return a state skeleton.
00:10:56 to which we can pass all of these props, such as an image, equal to, now we can provide different variations, such as light,
00:11:05 which is going to be forward slash images, forward slash light dash illustration, dot png, And we can duplicate this over for dark,
00:11:14 which is going to be dark illustration PNG.
00:11:17 And we can also give it an alt tag equal to empty state illustration.
00:11:24 Great.
00:11:25 So now since we already have one piece of data that I want to show, let's actually remove this if statement.
00:11:30 and make sure that we always return this state skeleton, just to make sure I can see it.
00:11:35 So if I go back to my browser, we can still see this question.
00:11:38 But now, if we head back over to the page where we're displaying those questions, instead of displaying this entire part,
00:11:45 which I will now comment out, You see it had the rendering of the questions, then it had no questions found, then it has error message.
00:11:54 Well, all of this will be within this single component now called the data renderer.
00:11:59 So here I can render the data renderer coming from components data renderer.
00:12:04 And to it, I need to pass all of these important props so it can display the data, such as success, error, data, which will be questions in this case,
00:12:14 but you can see that it is completely agnostic as to which data it displays.
00:12:18 That's why it's called generic data.
00:12:20 It can be questions, it can be tags, it can be profiles, whatever else.
00:12:25 We can also decide which empty state we want to show.
00:12:28 In this case, since we're working with questions, we know that it can be empty questions.
00:12:34 And finally, we can decide how we want to render those questions.
00:12:38 And that is by taking the questions array and then simply mapping over it.
00:12:42 I think we can basically just pull this part where we say questions dot map, and then we have a question card for each one.
00:12:49 If I now save this.
00:12:51 and go back, you can now successfully see this empty state.
00:12:55 So I'll actually put it to the side so we can see what we're working on in real time.
00:13:00 Now, I can head back over to the data renderer, and to it, I'll also provide some more things besides the image, such as the title equal to empty.title,
00:13:10 message equal to empty.message, and a button equal to empty.button.
00:13:16 And now we can display it within this skeleton or layout.
00:13:21 We can first style this outer div by giving it a margin top of 16, a flex container, w-full, flex-col so the elements appear one below another,
00:13:32 items-center to center it horizontally, justify-center to center it vertically, and on small devices, a bit of a larger margin top to divide it a bit from
00:13:43 the top.
00:13:44 Next, let's duplicate this image And we want to make it work on the light mode by saying images.light.
00:13:52 And it'll be hidden unless, and this one will actually be shown.
00:13:57 So we can say block, but on dark mode, we can actually make it hidden.
00:14:02 So if I save this and switch over to light mode, you can see that now we have a different illustration working perfectly.
00:14:10 Below this empty react fragment, we can display an H2.
00:14:13 That'll render a piece of text of title.
00:14:17 And give it a class name of h2-bold, text-dark 200, light 900, and a margin top of 8. There we go.
00:14:30 That's already looking so much better.
00:14:33 And finally, let's do a p tag that'll make it a bit funny where we can render the message saying something like, the question board is empty,
00:14:41 maybe it's waiting for your brilliant question to get things rolling.
00:14:44 And we can have a class name equal to body-regular.
00:14:49 Text-dark 500, light 700, margin Y of 3.5 to create some spacing, max-wmd, as well as text-center.
00:15:01 Looking good.
00:15:02 And finally, below the speed tag, if there is a button, In that case, render a new link component coming from NextLink that will render a button in within it,
00:15:16 coming from UIButton, and the button will simply render the button text, as buttons typically do.
00:15:23 Now, we'll have to fix this by giving this link an href, pointing to the button.href.
00:15:29 There we go, we can now see it.
00:15:31 Let's style it a bit by giving it a class name of paragraph-medium, margin top of 5, min dash h of 46 pixels, rounded dash lg,
00:15:46 bg dash primary 500, padding x of 4, padding y of 3, text dash light 900, on hover, bg dash primary dash 500. And now we have this beautiful Ask a Question button.
00:16:03 And if you click it, it actually redirects you to the Ask a Question page.
00:16:08 Again, it's the little details, but combine a few of these little details and they make all the difference.
00:16:14 And it's not only that it looks amazing and it feels amazing if we don't have any questions, But it's that it's going to be so simple to reuse this data
00:16:23 renderer wherever we need to.
00:16:25 You just have to call it like this, data renderer, render the data, and that's about it.
00:16:30 You no longer have to copy and duplicate this code everywhere else, just to make sure to display different errors and empty states.
00:16:38 So I will remove this from here.
00:16:42 And right now we should not be showing this state as we actually have some questions.
00:16:46 So I'll head back over to DataRenderer and I'll bring back this if statement saying that only if there's no data or if data.length is equal to zero,
00:16:57 then we display this state.
00:16:59 But else we actually have to render through the data.
00:17:02 And how do we render through it?
00:17:03 Well, check this out.
00:17:04 This is pretty cool.
00:17:06 We call the render function that we pass through props and then we pass in the data that we want to render.
00:17:13 And this is it.
00:17:14 We have our question.
00:17:16 Why does this work?
00:17:17 Well, that's because we passed the render function that we have declared right here before we actually call the data renderer.
00:17:25 And that way, you can make it fully agnostic to map over the question cards, or maybe if the data is something like tags,
00:17:32 you can simply change this over to tags.
00:17:35 Now, what happens if there's an error?
00:17:37 Well, we can very easily handle that as well.
00:17:39 By saying if, no success.
00:17:42 Very simple, right?
00:17:43 We can simply, again, render a state skeleton.
00:17:47 But this time not for an empty state, but rather for an error state.
00:17:51 So let me actually copy this entire state skeleton and let's change these things to match error states.
00:17:58 So I'll say light error, dark error, error state illustration.
00:18:03 And for the title right here, we can render the error question mark dot message or default underscore error.
00:18:12 .title in case we don't have a message.
00:18:15 Next, we have to render the message itself by saying error ?.details.
00:18:20 And if details exist, then we can render JSON.stringify error.details.
00:18:27 And we can further format it by saying null and two.
00:18:30 This is just a way to specify how it'll actually look like once stringified.
00:18:35 Or we render the default error.message.
00:18:38 And for the button, we say default underscore error dot button.
00:18:43 Great.
00:18:44 So now how can we mock this error to see whether it happens or not?
00:18:49 Well, we can head over to our actions.
00:18:52 So that's going to be question actions.
00:18:55 And I'll simply throw in an error right here at the top of the try block by saying throw new error.
00:19:02 That's going to say error.
00:19:04 As you can see, it breaks, but we get, ah, no questions yet instead of the error.
00:19:11 Why is that?
00:19:12 Oh, that's because I missed the return statement right here.
00:19:14 Of course, we have to return the UI.
00:19:17 And there we go.
00:19:18 But it looks like we have the wrong illustration right here.
00:19:22 It's images dash not dark dark and light dark.
00:19:26 This is wrong.
00:19:27 Instead, it should have been light error and dark error.
00:19:31 Hopefully, you noticed that before.
00:19:33 And now this works like a charm, and it's going to work even better because these error messages right here can be completely customized.
00:19:41 And we're already fetching the right error message because we're using the handle error functionality.
00:19:46 So whatever the real error is, it'll actually be shown right here to the user or to the developer.
00:19:54 So now we know that we're handling both the empty state and the error state, as well as displaying all of the questions right here.
00:20:01 Let's just make sure to provide a margin top right here to separate it from the tags.
00:20:07 We can do that in our homepage by wrapping the questions return within a div.
00:20:12 This div will have a class name of margin top of 10 flex w-full flex-col and a gap of 6. And now we can simply make this dynamic and put it within this
00:20:26 div right here.
00:20:28 If we go back, it's working like a charm.
00:20:30 And we can even filter based on specific tags or search.
00:20:34 Like if I type test, you can actually see no questions yet.
00:20:38 So this is actually perfect because even though we might have hundreds of questions, if someone searches for something that doesn't exist yet,
00:20:46 we can say no questions for that search query.
00:20:49 We can further optimize these states as ever we please because they're super scalable and super reusable.
00:20:55 Let's just make sure that we have properly styled the state skeleton.
00:20:59 So far, it looks good to me.
00:21:01 But we can always revisit it and make it even better.
00:21:04 With that in mind, you have now learned how to create a data renderer, which displays different empty and error states, or simply renders the data.
00:21:13 Be that questions, community, profiles, tags, or whatever else, you'll notice very soon how nicely it works and how simply you can reuse it.
00:21:22 So at this point, it's even hard to understand how cool what we have done right now is, but as we use it later on, you'll notice how great it is.
00:21:31 With that in mind, great work.
00:21:33 Our application is starting to look so much more like a finished product already.