20171130
Previous React App: linkedin-profile-editor
This is a react app so to run it you may need to ensure you have the necessary software installed
yarn is what we use in development so if you install that then navigate to this projects folder on your computer in a terminal and execute:
yarn start
it will boot up a server by default at http://localhost:3000 and open your default browser to that page.
There may be some other software that is required that I've missed but hopefully if you have yarn installed at least, attempting to run yarn start will hopefully provide some helpful output if there is anything else required.
making a react app that takes use of backend and frontend and uses a stock price api
This will link our knowledge with React front end so far with NodeJS backend from the previous week.
- create a new react app
yarn create react-app wolfofreact - replace most content in the app.js render and leave a single h1
- using from this api: https://api.iextrading.com/1.0/stock/aapl/quote
- an example of the data received for a stock: https://api.iextrading.com/1.0/stock/nflx/quote
- create a components folder and create a component file StockInfo.js
import React from 'react' function StockInfo({ symbol, companyName, primaryExchange, latestPrice, latestSource, week52High, week52Low }) { return ( <div> <h2>{ symbol }: { companyName }</h2> <h3>{ latestPrice }({ latestSource })</h3> <dl> <dt> Week 52 high </dt> <dd>{ week52High }</dd> <dt> Week 52 low </dt> <dd>{ week52Low }</dd> </dl> </div> ) } export default StockInfo
- initially just to get it to render on the page, just hard code some data for the component usage in App.js
import React, { Component } from 'react'; import StockInfo from './components/StockInfo'; import './App.css'; class App extends Component { render() { return ( <div className="App"> <h1 className="App-title">Wolf of React</h1> <StockInfo symbol='NFLX' companyName='Netflix inc' primaryExchange='Nasdaq Global Select' latestPrice={188.15} latestSource='Close' week52High={188.15} week52Low={188.15} /> </div> ); } } export default App;
- above
render (){in App.js added the following code to represent state:state = { quote: { symbol: 'NFLX', companyName:'Netflix inc', primaryExchange:'Nasdaq Global Select', latestPrice:188.15, latestSource:'Close', week52High:188.15, week52Low:188.15 } }
- In App.js add in the line
const { quote } = this.statewithin the render() block - Replace the App.js render hard coded data to take advantage of our state:
becomes
<StockInfo symbol='NFLX' companyName='Netflix inc' primaryExchange='Nasdaq Global Select' latestPrice={188.15} latestSource='Close' week52High={188.15} week52Low={188.15} />
<StockInfo {...quote} />
- Now we need some way to get the data from the API we chose
- https://github.com/axios/axios we'll be using this for handling the http GET requests (making use of the API)
- found the axios api, instructions are to use
npm install axiosbut we are using yarn and the command is the sameyarn install axios(when we run that in the terminal reminds us to useyarn addinstead ofyarn install) - make a folder in src called api
- make an iex.js file in the api folder
- with the following code:
which is the url before the /stock part
import axios from 'axios' const api = axios.create({ baseURL: 'https://api.iextrading.com/1.0' })
- Add the following function in iex.js file to handle the rest of the url:
export function fetchQuoteForStock(symbol) { //the fetch instead of get because // get implies immediate return, fetch will take some time to complete.. return api.get(`/stock/${symbol}/quote`) .then((res)=>{ return res.data //this is like going down a level because what we get returned is one level above what we wanted, ie, this gives us the json data instead of an object comprising that data }) }
- You can see now that the above two steps, taken together, reproduce the required api URL.
- you can bring that function into your App.js file with the following import line:
import { fetchQuoteForStock } from './api/iex';
- The following code added to the App.js file just above the
class App ...line can help us to test our function if you console log it:fetchQuoteForStock('nflx') .then((res) => {//using .then because the request will take some time to fetch //from the api server return res.data })
- remove our hardcoded values in the state part of App.js:
state = { quote: null }
- change the App.js return statement to handle when quote is null
return ( <div className="App"> <h1 className="App-title">Wolf of React</h1> { !!quote ? ( ///if the quote is there then load it <StockInfo {...quote} /> ) : ( //otherwise just display loading <p>Loading...</p> ) } </div> );
- you should now have a loading screen that never changes. Now we move onto making the loading actually load the data and display it.
- Adding a function in App.js (just below where we define the state within the Class App block) for when the component is initially mounted:
// the first time our component is rendered // this method is called: componentDidMount(){ fetchQuoteForStock('nflx') .then((quote) => { this.setState({quote: quote}) }) .catch((error) =>{ console.error(error) }) }
- Now you can remove the function declared previously in App.js at step 18:
fetchQuoteForStock('nflx') .then((res) => {//using .then because the request will take some time to fetch //from the api server return res.data })
- Add error handling to let the user know if something went wrong
- edit our state to:
state = { quote: null, error: null }
- amend componentDidMount to:
// the first time our component is rendered // this method is called: componentDidMount(){ fetchQuoteForStock('nflx') .then((quote) => { this.setState({quote: quote}) }) .catch((error) =>{ this.setState({error: error}) console.error('Error loading quote', error) }) }
- Amend your App.js code as follows:
const { quote, error } = this.state //'sugar' syntax for above. return ( <div className="App"> <h1 className="App-title">Wolf of React</h1> { !!error && <p> { error.message } </p> } {
- Add an input box for user entered symbols:
<div className="App"> <h1 className="App-title">Wolf of React</h1> <input value={ enteredSymbol } placeholder='Add api symbol here eg nflx' /> - Add a new variable to the state for the entered symbol:
state = { quote: null, error: null, enteredSymbol: 'Add api symbol here eg nflx' }
- At this point your field will not be editable.
- Add an event handler to handle when the field changes:
onChangeEnteredSymbol = (event) => { const input = event.target.value //now change state to reflect new value. // we don't need to do anything in this case that relies on the previous // state so we are just saying to set the value to the new value: this.setState({ enteredSymbol: input }) }
- Bring it together with the input box by amending the input box html code as follows:
<input value={ enteredSymbol } placeholder='Add api symbol here eg nflx' onChange = { this.onChangeEnteredSymbol } />
- We want to ensure they do not enter spaces so amend the event handler as follows:
onChangeEnteredSymbol = (event) => { const input = event.target const value = input.value.trim().toUpperCase() //do not allow spaces and make all uppercase //now change state to reflect new value. // we don't need to do anything in this case that relies on the previous // state so we are just saying to set the value to the new value: this.setState({ enteredSymbol: value }) }
- Add a button below our code for our input field:
As well as some styling in index.css which now looks like this:
<button> Load Quote </button>
html{ font-size: 30px; } body { margin: 0; padding: 0; font-family: sans-serif; } input{ font-size: 1rem; } button { font-size: 1rem; color: white; background: #222; border: none; }
- now create the event handling ability for the button:
added to index.css:
<button className = 'ml-1' onClick= {this.loadQuote}> Load Quote </button>
.ml-1{ margin-left: 0.25rem; }
- then create the event handler itself:
loadQuote = () => { const { enteredSymbol } = this.state fetchQuoteForStock(enteredSymbol) .then((quote) => {//using .then because the request will take some time to fetch //from the api server this.setState({quote: quote}) }) .catch((error) =>{ this.setState({error: error}) console.error(`The stock symbol '${enteredSymbol}' does not exist`, error) }) }
- Ensure that your default symbol in the field is set to a valid symbol, eg AAPL
and then we can now remove the repeated code in component mount function as it basically does the same thing as load quote, so it now looks like this:
state = { quote: null, error: null, enteredSymbol: 'AAPL' }
// the first time our component is rendered // this method is called: componentDidMount(){ this.loadQuote() }
- At this point is where the follow along ends and we start working on the challenges, if you need to view my solutions to the challenges you can check out the various commits starting around commit ‘Add company logo display’
-
Load and display logo for symbol using: https://iextrading.com/developer/docs/#logo
-
Add a history of previously loaded quotes
-
Add list of recent news using: https://iextrading.com/developer/docs/#news
-
Add 6 month table using: https://iextrading.com/developer/docs/#chart
-
Add 6 month chart using: https://iextrading.com/developer/docs/#chart
Nice charting library in React: http://recharts.org/#/en-US/guide/getting-started
I made a component to provide the code to display an image, using the url for the stock's company's logo. In the App.js file I was getting the logo's url as a returned json object {url: 'https://...'} but when I was calling on the component to render, I was using this notation
<StockLogo
{...logo}
/>And what that means is the logo objects key-value pairs are what get passed through as the components props, not the logo object itself.
In my StockLogo component's code I had this:
function StockLogo({
logo
}) {
return (
<div>
<img
className='stock-logo'
src={logo.url}
alt='no logo found'
aria-label='logo for the displayed company'
/>
</div>
)
}Which is going to be problematic because I've got logo there instead of url
As my code was at that point, the quickest solution to fix it would have been to just change the invocation of the StockLogo component to this code:
<StockLogo
logo={logo}
/>Alternatively, if I wanted to keep the syntax of {...logo} then my StockLogo component code would need to change to be like this:
function StockLogo({
url
}) {
return (
<div>
<img
className='stock-logo'
src={url}
alt='no logo found'
aria-label='logo for the displayed company'
/>
</div>
)
}I found myself trying to remove the 'loading...' message once the browser had already received a valid response. The way the program logic was initially meant that if the response was the data for a particular stock then when that loaded the loading message would be removed. But when the response is an error that the searched stock could not be found then the 'loading...' message remained.
My issue was that a ternery operator had been employed to determine if the 'loading...' message would display or if the stock data would display based on the test of whether the stock data was null or not.
I initially thought I could use an if statement or another ternary in that part somehow to additionally check that error was also null before displaying the message or not but found that that didn't work with the ternary syntax.
In the end a different approach was employed similar to one used for hiding elements where you use an expression like what is used in an if statement: this && that, that only runs if this resolves to true.
shout-out to Glenn for thinking of that one!
The code now looks like this:
(!!quote) ? ( ///if the quote is there then load it
<div>
<StockInfo
{...quote} //means my key value pairs become the props.
/>
<StockLogo
logo={logo} //passing through the actual object
// {...logo} //passing through the objects keyvalue pairs as the props
/>
</div>
) : ( //otherwise just display loading
!error && <p>Loading...</p>
)The reason this was so hard was that the ternary operator was going to execute one or the other of it's two possible outcomes, but in the event that an error is returned because the entered symbol wasn't found by the API then neither of the ternary's outcomes were appropriate.
Update: I had thought that it was not allowed to nest ternary operators but later found that if you're careful with your parentheses you can actually do just that with no problem.
This was not as straight forward as it initially seemed, there were two things that were tricky with this.
- Adding results to the history list, and
- displaying the history by iterating through. Adding results was tricky because for a beginner it was easy to write it in such a way that it wouldn't work and it wasn't immediately obvious why.
And iterating through the results to display them would have likely taken me a long time to figure out if I hadn't seen a colleague solve it already.
the code below works:
//example 1
const {history} = this.state
history.push(quote)
this.setState({quote: quote, error: null, history: history}) So does the below variation:
//example 2
this.setState({quote: quote, error: null, history: [...history, quote]}) but the below doesn't work:
//example 3
this.setState({quote: quote, error: null, history: history.push(quote)}) and neither does this:
//example 4
const {history} = this.state
newHistory = history.push(quote)
this.setState({quote: quote, error: null, history: newHistory}) If I had tried to tackle this myself the first thing I would have tried would have been example 3. That would have failed because the setState method only expects to be given objects, not code to evaluate. One day I'm sure I will know why history: [...history, quote] is not also considered in the same vein.
The reason I included example 4 as something not to do is because it's something I might've done. I mistakenly believed the .push method on an array would have returned a new array but that's not its behaviour at all, it modifies the array and does something like returns the resulting length of the array. This gave me some headaches with errors such as 'variable does not have a function .push'. It goes without saying that reading the documentation on the push method would have sufficed.
Below is the code that worked:
return(
<div>
{
history.map((e,i) => <li key={i}>{e.symbol}:{e.companyName}</li>)
}
</div>
)Again the reason I didn't think this would have worked is my flawed understanding of the map function. I thought that the map function would have modified the existing array, but inspecting the documentation reveals that it does return a new resulting array.
The other thing to note with return statements such as above is that the return statement can only return one html element, it's ok if that element contains many other elements but the following is not valid:
return(
<div>
{
history.map((e,i) => <li key={i}>{e.symbol}:{e.companyName}</li>)
}
</div>
<div>
this div is not valid
</div>
)If that's what you had you would have to wrap it all in another element like so:
return(
<div>
<div>
{
history.map((e,i) => <li key={i}>{e.symbol}:{e.companyName}</li>)
}
</div>
<div>
this div is now valid
</div>
</div>
)Initially I tried the following code:
return(
<div className='stockTable'>
<tbody>
<tr>
<th>Date</th>
<th>Open</th>
<th>Close</th>
</tr>
{
stockData.forEach(dayOfData => {
console.log(dayOfData.date)
<tr>
<td>{dayOfData.date}</td>
<td>{dayOfData.open}</td>
<td>{dayOfData.close}</td>
</tr>
})
}
</tbody>
</div>
)But that failed to render anything more than the headers, and resulted in numerous error messages in the browsers console. The first was <tbody> cannot appear as a child of <div> which is funny because I initially used <table> but it said <tr> cannot appear as a child of <table>. Add a <tbody> to your code to match the DOM tree
I realised after looking at a github post that I'm meant to have both <table> and <tbody>, with the latter inside the former.
Now I don't get any error messages related to the code in the StockTable component but the body of the table still isn't rendering in the browser...
After checking a few other stack overflow posts on the matter, it seems like the way to do this is to either use map, or use the foreach loop outside of the return statement to construct an array which you then use inside the return statement. The reason that map works is that map returns something whereas the foreach doesnt and also something to do with the return statement only being able to handle expressions, not functions and foreach is a type of function.
The below code works now where I've moved the loop out of the return statement and used it to build up an array, which is then used in the return statement:
export function StockTable ({
stockData //an array of objects consisting of data as bottom example comment shows
}){
const arrayOfDays = []
stockData.forEach(dayOfData => {
arrayOfDays.push(
<tr>
<td>{dayOfData.date}</td>
<td>{dayOfData.open}</td>
<td>{dayOfData.close}</td>
</tr>)
})
return(
<div className='stockTable'>
<table>
<tbody>
<tr>
<th>Date</th>
<th>Open</th>
<th>Close</th>
</tr>
{
arrayOfDays
}
</tbody>
</table>
</div>
)
}But that really doesn't look as clean as using the map way so going with this:
export function StockTable ({
stockData //an array of objects consisting of data as bottom example comment shows
}){
return(
<div className='stockTable'>
<table>
<tbody>
<tr>
<th>Date</th>
<th>Open</th>
<th>Close</th>
</tr>
{
stockData.map((dayOfData) => (
<tr>
<td>{dayOfData.date}</td>
<td>{dayOfData.open}</td>
<td>{dayOfData.close}</td>
</tr>
))
}
</tbody>
</table>
</div>
)
}Take note that in the above, the following block of code when moved out of the foreach loop and into the map function has its enclosing brackets changed from curly braces to parentheses: this is correct:
stockData.map((dayOfData) => (
<tr>
<td>{dayOfData.date}</td>
<td>{dayOfData.open}</td>
<td>{dayOfData.close}</td>
</tr>
))this is NOT correct:
stockData.map((dayOfData) => {
<tr>
<td>{dayOfData.date}</td>
<td>{dayOfData.open}</td>
<td>{dayOfData.close}</td>
</tr>
})I saw Glenn Marks' solution to this and really liked it so decided to imitate.
To get the ability to scroll through the past history means I will need to keep track of the position in the history that the user is currently at.
Initially AAPL will be the first stock searched by default.
If the user then searches for NFLX(Netflix) TSLA (Tesla) IBM(IBM) then after they hit search for IBM 'IBM' will still be what appears in the input box. We want to make it so that at that state, if the user selects the input box and presses the up arrow the contents of the input box change back to 'TSLA', if they hit up again: 'NFLX, and once more back to 'AAPL'.
If at that point they keep hitting the up arrow then no change should happen as that's the start of their history.
At that point they could then start hitting the down arrow to cycle back through their history in the opposite way ending up on IBM once more.
- maintain a history position counter (integer) which is incremented on each search and is initialised at zero.
- Upon page initialisation the position variable = 0, the history array length = 1 (from initial search for AAPL)
- the value of the position variable (0) coincides with the index of the history array's last element.
- upon up arrow press: attempt to decrement by one the history position variable. if doing so would result in its value being -1 then leave it at 0.
- update contents of the input field to match the history array's element at the index that coincides with the position variable.
- upon running a new search, set the position variables value to coincide with the history array's final element's index.
- only increment the position variable at the same time as we are adding a new element to the history array.