Getting Unstuck in React

Doug Schallmoser
7 min readNov 19, 2020

During a recent full build with React I ran into several issues that left me scratching my head for significant amounts of time. Some were common issues, and some were just particular to the application that I built. I want to share some of these hangups so that others may learn and grow from my frustrations. All of these issues were resolved by going down the rabbit hole that is debugging, along with tried and true Google searching.

The issues I want to discuss are as follows:

  1. useEffect and asynchronous functions
  2. createRef, scrollIntoView, stopPropagation
  3. google-maps-react and ReactDOM.render
  4. sending JWT tokens on every request that requires authorization
  5. passing down props vs global state (Redux), hooks

You’ll notice that some of these are not React issues and are in fact pure JavaScript and Rails topics. To provide some context, the application I built is called “Trailblaze”. It is a geosocial app designed to help people find new friends to go on outdoor adventures with. The frontend was built in React and the backend with Rails as an API. The app connects to three external API’s in order to provide an informative and user-friendly experience.

Trailblaze is a geosocial app allowing users to connect with other local outdoor enthusiasts.

Issue #1 — useEffect and asynchronous functions

useEffect is a React hook that allows us to run functions at specific times in a component’s lifecycle. This hook has similar functionality to the componentDidMount and componentDidUpdate lifecycle methods. The issue I ran into was different things updating at different times on the DOM. With useEffect, I was able to provide structure in when new results are displayed. In the first snippet, two API requests are made in the handleSubmit function (getUsers and getTrails) as well as an update to the Redux store (updateQuery). The issue was when the two API requests are made, queryData (Redux global state) has stale values since the dispatch to updateQuery has not yet resolved.

const handleSelect = async selection => {
setAddress(selection)
const response = await geocodeByAddress(selection)
const results = await getLatLng(response[0])
}
const handleSubmit = () => {
dispatch(updateQuery({ ...results, city: address }))
dispatch(getUsers(queryData))
dispatch(getTrails(queryData))
}

The second snippet fixes this issue by implementing useEffect for the API requests. This useEffect hook will run whenever queryData changes, which occurs when the handleSubmit function is executed.

const handleSelect = async selection => {
setAddress(selection)
const response = await geocodeByAddress(selection)
const results = await getLatLng(response[0])
}
useEffect(() => {
dispatch(getUsers(queryData))
dispatch(getTrails(queryData))
}, [queryData, dispatch])
const handleSubmit = () => {
dispatch(updateQuery({ ...latlng, city: address }))
}

Issue #2 — createRef, scrollIntoView, stopPropagation

In a social app like Trailblaze, a chat feature is almost essential to have. But with chat/text messaging comes a lot of scrolling. Most people don’t want to see the old messages and instead prefer to see the most recent messages as a default. The first image shows the beginning of the messaging history between two people. The second image shows the most recent messages, where the view should default to when this page is accessed.

When a conversation is clicked on, the default view cuts off the most recent messages.
After implementing scrollIntoView, the default view when a conversation is clicked on is the bottom of the page.

In order to accomplish this, I utilized a React Ref. Refs provide us access to the DOM whenever you need it but should be used sparingly.

const messageInput = React.createRef();return (
<div className="newMessageForm" ref={messageInput}
<form onSubmit={handleSubmit}>
<input type="text" value={message.text}
onChange={handleChange} className="message-input"/>
<div className="submit-container">
<input type="submit"
value="&#8593;" className="message-submit" />
</div>
</form>
</div>

You can see above that we utilize React.createRef() and set a reference where we ultimately want our default scroll to go to. Since the message input field is at the bottom of the page, I set the reference to be the div that holds that input field. Now that we have a reference, we can utilize useEffect to bring us to that reference because useEffect is executed whenever the component renders.

useEffect(() => {
messageInput.current.scrollIntoView();
})

Issue #3 — google-maps-react and ReactDOM.render

This issue is very specific to my project as well as the google-maps-react package. The image below illustrates the problem. The info window inside the Google Map provides information to the user. When a marker is clicked, the info window opens. Now, because of the way google-maps-react was written, onClick event’s cannot be created inside of the info window. Since all of the content inside the info window (including the “Add Favorite” button) is written inside the InfoWindow component, buttons that utilize onClick will not go anywhere.

The “Add Favorite” button inside the Google Map InfoWindow prohibits onClick events by default.

In order to solve this issue, I created a dummy div inside the InfoWindow component and passed a function to onOpen, as shown below:

<InfoWindow
marker={markerInfo.activeMarker}
visible={markerInfo.showInfo}
onOpen={e => {renderButtonInfoWindow()}}
>
... my div with text content here...
<div id="unique-placeholder" />
</InfoWindow>

The renderButtonInfoWindow function is invoked whenever a marker is clicked. The code at the bottom of this function utilizes ReactDOM.render just like when we are rendering our <App /> component to the div with the id of #root in our index.js file. This places our button (variable name “div”) right where we want it, nested inside of our dummy div.

const renderButtonInfoWindow = () => {
if (loggedIn()) {
const div = (
<button onClick={handleClickTrail} className="user-submit">
Add Favorite
</button>
)
ReactDOM.render(
React.Children.only(div),
document.getElementById("unique-placeholder")
)
}
}

Issue #4 — sending JWT tokens on every request that requires authorization

Trailblaze is a single page application that features user authentication . There are several ways of authenticating users but the method I implemented was utilizing JSON Web Tokens (JWT) and storing the token on the frontend in localStorage. This is not the most ideal nor most secure method, but it worked for the purposes of this build.

The thing that I learned after setting the backend up and successfully getting the frontend to persist a user’s data via their token, is that every request to the backend requires a new line in the fetch request headers (even GET requests). This makes sense as the user stores their token on the frontend, so in order to reach a backend action that requires authorization, some information (namely that token) needs to be sent to the backend along with the typical information.

An example fetch request is shown below. If a localStorage token exists, then we enter our fetch request block. The difference between a regular request and one with frontend JWT authentication is that we include a third key/value pair in headers.

componentDidMount = async () => {
const token = localStorage.token
if (token) {
const response = await fetch(`${API_ROOT}/conversations`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
})
const conversations = await response.json();
const userConvos = conversations.filter(convo =>
convo.author.id === this.props.currentUser ||
convo.receiver.id === this.props.currentUser)
this.setState({ ...this.state, conversations: userConvos })
}
}

Our Rails backend will check if the Authorization header exists, if it does it will decode the token and find the current user in the database based on their user_id which is retrieved from the decoded token.

def decode_token
if auth_header
token = auth_header.split(' ')[1]
begin
JWT.decode(token, 'application_secret', true, algorithm: 'HS256')
rescue JWT::DecodeError
nil
end
end
end

Issue #5— passing down props vs global state (Redux), hooks

This is something that developers consider before they start building their application. I initially thought I would be better off not using global state from the start and only implementing it when prop drilling occurred. The issue with that is you could be so far into the build that a major refactor may be required to change from local to global state. In an ideal world you would not utilize global state and only utilize local state. But at the beginning of a project it can be difficult to recognize what you will need access to and where. I recommend spending a little bit extra time brainstorming at the beginning, and implementing some global state for the things that most likely require it.

In my application I initially setup global state for the search query and the current user. I knew I would need access to the search query in multiple components because all of the data that makes this application useful stems from the query. And since there are user accounts, it only makes sense to have global state for the current user since the user’s information is required in many components for authorization purposes. I eventually added arrays of trails and search results to the global state. While I could have made them local state, I know that in the future I will be adding new features that will require them to be accessible globally.

const initialState = {
currentUser: {},
trails: []
results: [],
query: {
lat: '',
lng: '',
radius: 10,
agemin: '',
agemax: '',
gender: 'any'
},
}

Here is an example of utilizing local state. This component renders the Favorite component with several props passed down, including functions.

Passing down props is preferred and callback functions can be passed down as well.

And here is an example of global state. When a search filter is applied, the search query global state is updated. This allows for cleaner code that does not prop drill incessantly.

Global state is useful when multiple components require access to state but are far apart in the relationship tree.

Thanks for reading!

Trailblaze allows you to add trails to your favorites.

The Github repository for this application can be found at the link below:

https://github.com/dougschallmoser/trailblaze-react-app

--

--