Sometimes it's fun to put the big projects aside for a while and make something small. That's not to say it can't be challenging - it's most of the time during these small endeavours that I find myself learning something new that I may have been putting off for a while. This is the joy of not having the distraction of scores of components, state, props and more.
For no specific reason, I was inspired to make a dynamic search bar which does three things:
- Takes a text input
- Filters the results containing said text
- Highlighting that very text
I'd made something similar to this a long time ago in vanilla JS, but I don't remember how exactly (and chances are I won't want to).
However, it was something I hadn't needed up until now in a React project so I thought it would be a good use of time in case, you know, the time ever comes.
Being the "bish-bash-bosh" project this was I stuck with React and React alone. OK, there's obviously some styling, but nothing more than a few imported Material UI components. This really was more about the functionality then anything else.
We will also be making use of some JSON placeholder from this JSONplaceholder website to populate our app. We're pulling from the following API:
This will deliver back to us an array of objects, each like so:
The file structure for this project is as follows:
Let's go through the components before diving into
App.js, where the bulk of our logic sits, so we can gain an understanding of what's going on in each.
Let's take a look at
Before we move on, I just want to point out that
classes.* are all related to Material UI and not important to what's going on. You can think of them as almost any HTML element you like.
With that aside, let's look at what is important.
Well, if we were to look at this without all of the additional styling or function we would have something like this.
So, for the most part, this component is essentially our container for each of our objects we receive back from our JSON placeholder API. These values are being passed into the component via
props and rendered as we choose.
We'll come back to the slightly more complex version once we've look over the rest of our components.
SearchBar.js is an even more compact component. Beautiful!
Again, please note that the
Textfield element is a Material UI component, and could just as easily be an
input element with the
prop that is passed to this component is via
props.onInput, which is responsible for updating our state each time a new character is typed into or deleted from our input field.
Our last component is
Counter.js. This component isn't strictly required to make this project work, however I thought it was a nice touch.
You know the deal with the Material UI stuff by now!
prop this time. We're simple passing in a result, and we'll come back to exactly what that is very soon.
OK, it's time for the big one. Let's move on to
App.js. For the sake of readability we'll break this down into smaller sections as it's a fair bit larger than the previous components. Not humongous, but bigger nonetheless.
This part of the app makes use of the
useReducer hooks provided natively with ReactJS, so we'll start by importing those. We then bring in our 3 components we just went through to complete our imports.
As the functionality for this project was all crammed into the
App component, I decided to opt for
useState to save from having four separate state variables, though it could just as well have been implemented that way too.
If you're familiar with
useReducer you can skip along to the Continuing With The App section. Just take note of the code above and the coming snippets.
We start by declaring our
initialState for the component which consists of four different keys - so what are they for?
isLoadingaccepts a boolean value to essentially let our app know whether the async function has completed or not - or is loading.
datawill be our store for the array we receive back from our API call.
searchwill hold the string which is entered into the
searchDatawill be a filtered version of our data state array. This will remain an empty array until something is entered into the search input.
reducer function is the tool we use to alter or update our state object as necessary. A note here, you should declare both your
initialState object and
reducer function outside of the component itself. If you are familiar with how
useState works then you're in a good position to understand
useReducer as the two are very similar. I'll explain how.
I mentioned before that this could have just as easily been implemented with
useReducer, and here's an example of how the two compare. Both of the code examples below have one thing in common - in the
useReducer example the
isLoading key/values are able to hold the exact same information as the
isLoading variables in the
useState we are provided a function, which we name, as a return value from
useState(). This function is how we update the value of state, for example
setData(data) would update our
data state to contain (in this example) the array returned from our API call, and then we could call
setIsLoading(false) which would update the
isLoading variable from true to false.
useReducer we need to provide a reducer function (which we did in our code snippet above) to update the state object. This has the added benefit of being able to update multiple states at once. Take a look at
case "SET_DATA": in our snippet.
In the return value we start by passing in the initial state using the ES6 spread operator. This essentially ensures we start where we left off and pass all existing state values back into the object we want to return. We then pass in the key/value pair of
data: action.payload. This updates the current value of
data to the one which we pass in when we call the
reducer function (which we'll come to soon). In the same
return, we are also able to update
false to end the loading sequence.
All that's left to do is use the
useReducer function like so :
This gives us access, in the same was as
useState, to our
initalState (and object in this case stored in the state variable) and a function to update our state (in this case stored in
dispatch). We pass in our reducer function and
intialState and we are ready to go! We can now call
dispatch in our component which will fire off our
reducer function and update our
We need to pass in the "type" of update we wish to be carried out and, where applicable, the "payload" for the update.
type is determined in the
switch statement of the
reducer function, and
payload is a fancy word for the data we want to store there (be it an array, boolean, string, etc.) And that's state updated!
Hopefully, you can see how
useReducer could be beneficial. As the complexity of your app and its state grow, and the relationship between those states becomes stronger, you will inevitably find that
useReducer is superior in handling the growing workload. Of course, you would likely want to incorporate a level of error checking to this but for the sake of this project this was sufficient.
Now we've got a home for our state and the ability to update it we can move on to the functionality. I won't go into how the data is fetched from the API, there are a million tutorials, blog posts and docs on that. All that you will want to know is that we use the
dispatch example above to get that data into our state.
The return statement for our
App component contains our
Item components. Let's go through each one and start connecting the dots.
We'll start with our
SearchBar component and the function that is called within its
onInput attribute. As you'll remember, we passed a prop down to this component via
props.onInput and this allows us to call the following function when we type something into our text input:
Woah! That's a lot of code for an input. Well, this function does a little more that just deal with the input itself. Let's deal with that first though, and it's a pretty small part of the function.
On the second line of the function we declare a variable
str and assign it
e.target.value which simply keeps the string as it is entered into the input field. On the following line we call our dispatch function (go back through the A Note On useReducer section if you've no idea what that means) and pass the type of
payload the value of
str. This, together, updates our state to always store the most up-to-date string in the input field.
The next part of the function deals with the filtering of our data array, stored in
.filter() method to iterate through the
body values of our objects and see if the text in our
.include() method anywhere in their respective string. The addition of the
.toLowerCase() method ensures that no matter what casing we use when we type into the search bar, if the letters themselves match our filtering will be successful. Without this a search for "Hello World" would not return the result "hello world" - and we don't want to be that pedantic with our users!
.map() method on
state.data to iterate though each of the filtered objects and apply our highlighting.
This took me a great many attempts to get right, and part of me wishes I could have found a way to do it using only the strings themselves, however I had to call upon the dreaded
dangerouslySetInnerHTML to make this work.
At the beginning of this article I showed you the following code:
This is our
Item component, and you've likely noticed that two of the elements make use of
dangerouslySetInnerHTML to populate themselves. If you want to read more about
dangerouslySetInnerHTML then I suggest checking out the official docs. However, we will make the assumption in our case that we trust our source and the content it provides.
createMarkup function returns an object with the key of
__html and value of the HTML itself, as recommended in the React docs, and this value is used to set the inner HTML of each element. This approach turned out to be necessary to be able to inject a
<mark> element into the string to function as our highlighter.
.replace() method to highlight our strings, therefore we start by declaring a new variable for the value we will have returned to us by this method.
.replace() takes in two arguments, the first of which is the pattern which we want replaced. This could simply be a string or, as is our approach, a
RegExp itself takes two arguments - firstly the string (or pattern) we want to identify, and secondly some options (or flags) to give the
RegExp some guidance on what we want done. In our case we pass the string
"gi". This does two things. The g tells the
RegExp that we want to search the entire string and return all matches, and the
i that our search should be case-insensitive and without this, like if we were to omit the
.toLowerCase() method from our filter, we would not highlight words regardless of their case.
One the has
RegExp has identified the characters we would like to replace it moves on to the second argument in the
.replace() method, which is the what should replace that. This is where and why our use of
dangerouslySetInnerHTML was necessary as we are inserting the
<mark> tag back into our object. Without this we would actually render the characters around our string on the screen.
This second argument is a function with the parameter of
match. This allows us to re-purpose our original string, wrap it in the new HTML element, and return it. These new values are now the values stored in the
newBody variables. We can now simply return these back into the
newArr constant in our return statement, being careful not to overwrite our original object values using the spread operator:
The final piece to this function is to dispatch our new array
newArr of filtered and highlighted objects into our state.
Now all that's left is to render the results.
This nested ternary operator asks two questions to decide what to do. Firstly, have you finished loading yet? Yes? Right! Then, have you typed anything into the search field (
state.search.length > 0 ?)? Yes? In which case, I'll run through everything that's now in
state.searchData (including their new title and body values and their
<mark> elements you filtered out) and generate your
Items for you.
Voila! Would you look at that!
And if there isn't anything in the search bar? Well then, I'll just render everything you have stored in
data. This is completely unfiltered and untouched.
But, what happens if I do type something into the search but it doesn't have any matches? Well, typing into
SearchBar will mean that our ternary operator will see that there are characters in our
state.searchand render everything in the array...nothing!
The counter shown in the examples above is more of a nice to have, but in some cases it might be useful to give the user some idea of how may items they have filtered down to. For example, typing the string "aut" into my search gives me 66 matches. Maybe I could be more specific before trying to scroll through all of that data. Oh yeah, "aut facere" gives me only 2 results! Great.
This is a simple little component which simply gets passed the length of the
state.searchData array (or nothing, if there isn't anything, to save displaying 0 all of the time).
Here's the component itself:
And its implementation into
And that’s it! I hope I was able to share something interesting with you here, and I’d really appreciate any feedback on either the content or the writing. I’d like to do this more often and making it worthwhile would be a massive bonus.