Simple weather app using create-react-app in CodeSandbox.
Develop a weather application with the following specifications:
- It should show the 5-day weather forecast for Dallas, TX.
- Your application should utilize ReactJS (usage of boilerplates such as Create React App are encouraged).
- You should be able to toggle between Fahrenheit and Celsius.
- It should match the provided comp as closely as possible on desktop.
- We do not provide a mobile mockup as you will determine how that functions on your own.
- Icons should correspond to proper weather conditions.
- It should be responsive, rendering appropriately on tablet, and handheld form factors.
- Your application may use any open weather API.
- Your application should showcase one animation technique of your choosing in order to give the application some life.
- (Optional) Allow for user input to change location of forecast (city, state, or zip)
- Initial research.
- API: weatherbit
- XHR: axios
- Styles: styled-components
-
State management: mobx
- Setup base-level project scaffold.
- Plan out components.
-
AppDay.jsx
- Displays a single day of the upcoming 5-day week. It visually shows a user an abbreviated day name, an icon representing the day's projected weather conditions, and the temperature from the api call. -
AppGraphic.jsx
- Displays a static image representing the city/location selected; Dallas in this example. - ...
-
- Develop base-level app wireframe.
- Install
styled-components
package. - Fine-tune scaffold and components.
- Polish stylesheets.
- Download assets from Zelplin.io provided comp.
- Integrate weather API.
- Integrate accessibility.
- Integrate Schema/SEO.
- Integrate webapp (SPA) icons into HTML file.
- Integrate meta tags into HTML file.
- Refactor for responsiveness.
- Refactor for semantic HTML.
- Enhance with animations.
- Write JEST tests.
- Test on mobile & tablet devices.
- Test for WCAG & accessibility.
- Browser checks.
Coming off the heels of a data/api-driven Vue app, a serious hurdle I found myself struggling to get over was how to work with an Api and handle central state management in React. Vuex, Vue's solution to a store, made mutating central state a breeze.
Reading the Api response and selecting the correct index from the next weekday was initially confusing. Weatherbit's Api for a 5-day forecast returns an array (response.data.data
) length of 39. Initially, when passing data into my AppDay
component props, I went with the following:
// omitted...
<Week>
<AppDay day={api.data[1].timestamp_utc} />
<AppDay day={api.data[2].timestamp_utc} />
<AppDay day={api.data[3].timestamp_utc} />
<AppDay day={api.data[4].timestamp_utc} />
<AppDay day={api.data[5].timestamp_utc} />
</Week>
// etc...
My thought was, "Okay. Today is obviously going to be api.data[0]
, so the next likely sequence would be [1]
, [2]
, [3]
, and so on. It didn't initially occur to me why a 5-day forecast's data child array would contain 39 entries (haha, silly me...). The length comes due to the endpoint returning 3-hour intervals for daily weather. So, depending on the time you ping it'll return a different response length. So, to resolve this, I opted for using the same service's 16-day/daily forecast endpoint. Now each entry is a single day, therefore my Week
can be structed like I had originally anticipated.
Despite all this, I eventually refactored my <AppDay />
component calls into a .map
loop—which presents a cleaner source code:
const data = this.state.data;
const week = _.get(data, "data", []).slice(1);
<Week>
{week.map((data, i) => (
<AppDay
key={i}
day={data.valid_date}
degrees={data.temp}
icon={getIcon(data.weather.code)}
tooltip={data.valid_date}
/>
))}
</Week>;
A free plan from weatherbit allows for 1,000 calls/day; therefore I needed a way to limit the number of calls I make. Since CodeSandbox can refresh you project's frontend view on the fly, not setting a limiting function could (and did, haha) send me requests through the roof! I did some Google-foo and came up with the following function:
// omitted...
/**
* Sets date timestamp tokens to localStorage. Compare token to
* `threeHoursAgo`. If true, refresh our API.
* @method refreshApi
* @see [StackOverflow]{@link https://stackoverflow.com/a/42529483}
*/
const refreshApi = () => {
const HOUR = 1000 * 60 * 60;
const THREEHOURS = HOUR * 3;
const threeHoursAgo = Date.now() - THREEHOURS;
const token = localStorage.getItem("token");
if (!token) localStorage.setItem("token", new Date());
if (token < threeHoursAgo) {
localStorage.setItem("token", new Date());
return true;
} else {
return false;
}
};
This little guy simply constructs a 3-hour variable, THREEHOURS
, and a variable, threeHoursAgo
that subtracts that from Date.now()
. It uses threeHoursAgo
in conjunction with a localStorage token
to return a boolean. If the boolean returns true we'll refresh our Api call:
useEffect(() => {
if (refreshApi() === true) fetchData();
}, []);
The app requires that icons, "... correspond to proper weather conditions." Considering there are a possible 38 different codes coming down from the api, and that we currently only have four icons available, a solution was required to ensure an icon displayed on the frontend regardless of the response.data.weather.code
value. My solution was a helper function that takes in a number parameter and returns a filename string depending on the group which the param belonged to.
const getIcon = code => {
const icons = {
cloudDrizzle: "cloud-drizzle-sun",
cloudDrizzleSun: "cloud-drizzle-sun",
cloudLightning: "cloud-lightning",
cloudSun: "cloud-sun"
};
const groups = {
drizzle: [300, 301, 302],
general: [800, 801, 802, 803, 804, 900],
hazards: [700, 711, 721, 731, 741, 751],
rain: [500, 501, 502, 511, 520, 521, 522],
snow: [600, 601, 602, 610, 611, 612, 621, 622, 623],
thunderstorms: [200, 201, 202, 230, 231, 232, 233]
};
if (groups.drizzle.includes(code)) return icons.cloudDrizzle;
else if (groups.general.includes(code)) return icons.cloudSun;
else if (groups.hazards.includes(code)) return icons.cloudSun;
else if (groups.rain.includes(code)) return icons.cloudDrizzle;
else if (groups.snow.includes(code)) return icons.cloudDrizzle;
else if (groups.thunderstorms.includes(code)) return icons.cloudLightning;
else return icons.cloudSun;
};
Basically, I defined an icons
object that contained key/value pairs of our available icon SVGs. I then constructed a groups
object which contained child arrays that represent high-level definitions of weather conditions; such as drizzle
or snow
.
Now, using a switch
statement here would be ideal—however, it could be hacky and abusive of the way case
is evaluated (as our conditional relies on array.includes()
). Therefore, I crafted an incredibly ugly if/else
chain to determine the return output from our function. Some examples this in use are:
getIcon(300); /** @returns {cloud-drizzle-sun} */
getIcon(802); /** @returns {cloud-sun} */
getIcon(511); /** @returns {cloud-drizzle-sun} */
getIcon(233); /** @returns {cloud-lightning} */
Handling the functionality to switch between Fahrenheit and Celsius proved particularly difficult.
First, I needed to work with use-persisted-state to retrieve and store both types of JSON strings from the Api.
// before celcius integration
const useWeatherState = createPersistedState("api");
const [api, setData] = useWeatherState();
// after celcius integration
const useWeatherState = createPersistedState("api"),
useWeatherStateC = createPersistedState("apiC");
const [api, setDataF] = useWeatherState(),
[apiC, setDataC] = useWeatherStateC();
Next, I needed to find a way to swap between using api
and apiC
as dynamic parent objects to pass down the data. My first try revolved around setting up a conditional check and a two mutable let
declarations:
let api;
const selection = localStorage.getItem("degrees");
if (selection === "F") api = apiF;
else if (selection === "C") api = apiC;
else api = apiF;
let state = {
cityName: api.city_name,
stateCode: api.state_code,
today: {
temp: api.data[0].temp,
date: api.data[0].valid_date,
conditions: api.data[0]
}
// etc...
};
This worked on page refresh, however it did not dynamically through a handleDegreesChange(event)
function. Therefore, I needed to take a different approach. I recalled the section from the React docs on Lifting State Up, which presents the idea of sharing state, "...by moving it up to the closest common ancestor of the components that need it." This also lit my brain lightbulb to realize I needed to be using setState
in this scenario to swap the root data the React way instead of my silly let
conditional. You'd think this would all be ovbious—however, I am a novice in React; having spent the entire time of my last enterprise app development in a Vue environment.
So, let's take a look at how I refactored to using setState
.
// index.js
import AppRoot from "./App";
function App() {
// omitted...
return <AppRoot data={api} dataC={apiC} />;
}
CodeSandbox's create-react-app boilerplate sets up a root function declaration for our project, aptly titled function App()
. My project's source-of-truth parent component is imported as AppRoot
and then returned with two prop attributes: data
(for our base/Fahrenheit api data) and dataC
for our metric Celcius api data. So we pass those on down into our App.jsx
component:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: '',
scale: localStorage.getItem('degrees'),
units: ''
};
}
// ...