React/redux quote machine - thoughts

I’m still working on this but most of the react/redux is probably done. I have to work on the css and maybe add some to the react code to get a background color change when the ‘new quote’ button is clicked.
I use an incrementer state variable to track the event of the ‘new quote button’ being pressed. I’m not sure if that’s the best way. I’m not sure any of this is the best way, but I feel like I’ve learned a lot so far just working on this little app. Any feedback/critiques appreciated. Thanks!


class App extends React.Component {
  
	constructor(props) {

    super(props);

  }
  
  render() {

    return (
        <div className="container mx-auto p-5 my-3 bg-dark text-white">
					<QuoteBoxContainer />

					<ControlsContainer  />
				</div>
    );
  }
}

class QuoteBox extends React.Component {
  
	constructor(props) {

    super(props);

    this.state = {
      items: {}
    };
    
    this._rotateQuote = this._rotateQuote.bind(this);
    this._fetchRandomQuote = this._fetchRandomQuote.bind(this);
  }
	
	componentDidMount() {
  	
		let that = this;
    
   	fetch('https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json').then(function (response) {
        return response.json();
      }).then(function (result) {
				
      	that.setState({
            items: result.quotes
          });
          
        //that.props.dispatch({type: 'IS_LOADED', payload: true});
        that.props.setIsLoaded();
        that._rotateQuote();
      });
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
  	
  	//componentWillUpdate is deprecated so I guess this is the best method to do this in?
  	//Even though it seems ineffecient since it requires an extra re-render

  	if (prevProps.quoteCounter < this.props.quoteCounter) {
  		this._rotateQuote();
  	}
  	
  }
  
  /* private method */
	_rotateQuote(){
		
		let quoteObj = this._fetchRandomQuote();
		
		// this might not be the best way to do this but as long as the list of quotes is not tiny it won't loop too often.
		// We could just iterate through sequentially but then it's not really a random quote.
		while (quoteObj.quote == this.props.quote) {
			quoteObj = this._fetchRandomQuote();
		}
		
		this.props.setNewQuoteData(quoteObj);
  }
  
  /* private method */
  _fetchRandomQuote(){
  	return this.state.items[Math.floor(Math.random() * this.state.items.length)];
  }
  
  render() {
		
    return (
			<>
				<div className="row">
					<div className="col mx-auto text-center fa fa-quote-left" style={{}}><Quote quote={this.props.quote}/></div>
				</div>
				<div className="row">
					<div className="col text-center">-<Author author={this.props.author}/></div>
				</div>
			</>
    );
  }
};

const Quote = (props) => 
	<span id="text" className="p-3">{props.quote}</span>


const Author = (props) => 
	<div id="author">{props.author}</div>


const Controls = (props) => {

	return (
		<div className="row align-items-center">
			<div className="col"><NewQuoteButtonContainer/></div>
			<div className="col text-right"><TweetButton quote={props.quote} author={props.author} /></div>
		</div>
	);
};


const TweetButton = (props) => {

	let url = 'https://twitter.com/intent/tweet?hashtags=quotes&related=freecodecamp&text=' + encodeURIComponent('"' + props.quote + '" ' + props.author);
	
	return ( 
			<a href={url} className="" id="tweet-quote" title="Tweet this quote!" target="_blank">
  			<i className="fab fa-twitter"></i>
			</a>
	); 
};

const NewQuoteButton = (props) => {

	return (
		<>
			<button className="btn btn-default button" id="new-quote" onClick={props.incrementQuoteCounter}>New Quote</button>
		</>
	);
};

const quoteReducer = (state = {quoteCounter: 0, isLoaded:false, author:'', quote: ''}, action) => {

  switch (action.type) {
    case 'INCREMENT_QUOTE_COUNTER':
      return Object.assign({}, state, {quoteCounter: state.quoteCounter+1});
    case 'SET_IS_LOADED':
      	return Object.assign({}, state, {isLoaded: true});
    case 'SET_NEW_QUOTE_DATA':
      return Object.assign({}, state, {author: action.author, quote: action.quote});
    default:
      return state;
  }
};

// React:
const Provider = ReactRedux.Provider;
const connect = ReactRedux.connect;

const store = Redux.createStore(quoteReducer);

const mapStateToProps = (state) => {
  return {quoteCounter: state.quoteCounter, isLoaded: state.isLoaded, author: state.author, quote: state.quote}
};

const mapDispatchToProps = dispatch => ({

  setIsLoaded: () => dispatch({type: 'SET_IS_LOADED', payload: true}),
  
  setNewQuoteData: (quoteObj) => dispatch({type: 'SET_NEW_QUOTE_DATA', author: quoteObj.author, quote: quoteObj.quote}),
  
  incrementQuoteCounter: () => dispatch({type: 'INCREMENT_QUOTE_COUNTER'})
});

const NewQuoteButtonContainer = connect(mapStateToProps, mapDispatchToProps)(NewQuoteButton);

const QuoteBoxContainer = connect(mapStateToProps, mapDispatchToProps)(QuoteBox);

const ControlsContainer = connect(mapStateToProps, mapDispatchToProps)(Controls);

class AppWrapper extends React.Component {

  render() {
    return (
      <Provider store={store}>
        <App/>
      </Provider>
    );
  }
};

ReactDOM.render(
  <AppWrapper/>,
  document.getElementById('app')
);


Writing in vanilla JS (or slowly dying jQuery) is IMHO sufficient for this challenge, and should be (marginally) faster than using frameworks. You only need to change the quote and the author–background is optional and aesthetic–but even those three can be done without React/Redux or other frameworks.

I know but I want to focus on learning react/redux. And I thought it would be a good idea to learn the concepts while implementing a simple app like this one. This way I could focus on learning about react/redux rather than having to deal with the problems i’d encounter in a more complex app.