Skip to content

Instantly share code, notes, and snippets.

@stevekinney
Created June 27, 2019 11:22
Show Gist options
  • Save stevekinney/b819d360e103847cbaab63b5bdf182a6 to your computer and use it in GitHub Desktop.
Save stevekinney/b819d360e103847cbaab63b5bdf182a6 to your computer and use it in GitHub Desktop.

Tweet Stream (Redux Thunk)

Let's start by pulling in the middleware and the actual thunk library.

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

Now, we can pop that middleware right in there.

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

We'll start with a pretty silly action inactions.js:

export const fetchTweets = () => {
  return (dispatch) => {
    fetch('https://tweet-stream.glitch.me/api/tweets')
      .then((response) => response.json())
      .then((response) => {
        console.log(response, dispatch);
      });
  };
};

We'll also make a quick and dirty <FetchTweets> component:

import React from 'react';
import { connect } from 'react-redux';
import { fetchTweets } from './actions';

const FetchTweets = ({ fetchTweets }) => {
  return (
    <section className="FetchTweets">
      <button onClick={fetchTweets}>Fetch Tweets</button>
    </section>
  );
};

export default connect(
  null,
  { fetchTweets },
)(FetchTweets);

Okay, now—pop that into index.js in the <Application> component and we should be good to go.

const Application = () => {
  return (
    <div className="Application">
      <h1>Tweet Stream</h1>
      <FetchTweets />
    </div>
  );
};

Sweet. We can hit the button and see stuff in the console, but how do we get stuff into our Redux store?

Eh, first let's get the receiving end in place.

import React from 'react';
import { connect } from 'react-redux';

import Tweet from './Tweet';

const Tweets = ({ tweets = [] }) => {
  return (
    <section className="Tweets">
      {tweets.map((tweet) => (
        <Tweet key={tweet.id} tweet={tweet} />
      ))}
    </section>
  );
};

const mapStateToProps = (state) => {
  return { tweets: state.tweets };
};

export default connect(mapStateToProps)(Tweets);

Okay, now let's update those actions:

export const ADD_TWEETS = 'ADD_TWEETS';

export const fetchTweets = () => {
  return (dispatch) => {
    fetch('https://tweet-stream.glitch.me/api/tweets')
      .then((response) => response.json())
      .then((response) => {
        dispatch(addTweets(response.tweets));
      });
  };
};

export const addTweets = (tweets) => {
  return {
    type: ADD_TWEETS,
    payload: { tweets },
  };
};

Finally, we need to update the reducer to get it all working.

import { combineReducers } from 'redux';
import { ADD_TWEETS } from '../actions';

const tweets = (tweets = [], action) => {
  if (action.type === ADD_TWEETS) {
    return action.payload.tweets;
  }

  return tweets;
};

export default combineReducers({
  tweets,
});

Cool. So, it looks like we have a basic set up in place.

Multiple Dispatches: Loading Status

Let's think through all of the ways this could go wrong with our actions:

export const SET_STATUS = 'SET_STATUS';
export const NOT_LOADED = 'NOT_LOADED';
export const LOADING = 'LOADING';
export const LOADED = 'LOADED';
export const ERROR = 'ERROR';

export const setStatusToLoading = () => ({
  type: SET_STATUS,
  payload: { status: LOADING },
});

export const setStatusToLoaded = () => ({
  type: SET_STATUS,
  payload: { status: LOADED },
});

export const setStatusToError = () => ({
  type: SET_STATUS,
  payload: { status: ERROR },
});

export const resetStatus = () => ({
  type: SET_STATUS,
  payload: { status: NOT_LOADED },
});

Let's all set up some super simple logic into our reducer:

const status = (status = NOT_LOADED, action) => {
  if (action.type === SET_STATUS) {
    return action.payload.status;
  }

  return status;
};

export default combineReducers({
  tweets,
  status,
});

We'll make a <LoadingStatus> component:

import React from 'react';
import { LOADING } from './actions';
import { connect } from 'react-redux';

const LoadingStatus = ({ status, children }) => {
  if (status === LOADING) {
    return (
      <section className="FetchTweets">
        <button disabled>Loading…</button>
      </section>
    );
  }

  return <>{children}</>;
};

export default connect(({ status }) => ({ status }))(LoadingStatus);

Finally, we'll wrap the button in index.js.

const Application = () => {
  return (
    <div className="Application">
      <h1>Tweet Stream</h1>
      <LoadingStatus>
        <FetchTweets />
      </LoadingStatus>
      <Tweets />
    </div>
  );
};

Redux Observable

Okay, we're going to need some different middleware:

import { createEpicMiddleware } from 'redux-observable';

const epicMiddleware = createEpicMiddleware();

And we'll plug that shit into Redux as middleware.

import { createStore, applyMiddleware } from 'redux';

const store = createStore(
  rootReducer,
  applyMiddleware(epicMiddleware)
);

So maybe then we kick off a root epic—shall we?

import { rootEpic } from './epic';

epicMiddleware.run(rootEpic);

Okay, so—we'll start with our actions as usual:

export const FETCH_TWEETS = 'FETCH_TWEETS';
export const FETCH_TWEETS_FULFILLED = 'FETCH_TWEETS_FULFILLED';

export const fetchTweets = () => ({ type: FETCH_TWEETS });

export const fetchTweetsFulfilled = (payload) => ({
  type: FETCH_TWEETS_FULFILLED,
  payload,
});

Let's also put a super simple epic in place:

import { ajax } from 'rxjs/ajax';
import { ofType } from 'redux-observable';
import { map, mergeMap } from 'rxjs/operators';
import { FETCH_TWEETS, fetchTweetsFulfilled } from './actions';

const fetchTweetsEpic = (action$) =>
  action$.pipe(
    ofType(FETCH_TWEETS),
    mergeMap((action) =>
      ajax
        .getJSON('https://tweet-stream.glitch.me/api/tweets')
        .pipe(map((response) => fetchTweetsFulfilled(response))),
    ),
  );

export default fetchTweetsEpic;

Cool, now let's give it whirl in the reducer.

import { combineReducers } from 'redux';
import { FETCH_TWEETS_FULFILLED } from '../actions';

const tweets = (tweets = [], action) => {
  if (action.type === FETCH_TWEETS_FULFILLED) {
    return action.payload.tweets;
  }
  return tweets;
};

export default combineReducers({
  tweets,
});

Sweet. Now it's working.

Loading Status

Well, we have an interesting thing here. We're firing off two events: one for when it triggers the request and one for when it lands.

We'll need that <LoadingStatus> component again:

import React from 'react';
import { connect } from 'react-redux';

const LoadingStatus = ({ status, children }) => {
  if (status === 'LOADING') {
    return (
      <section className="FetchTweets">
        <button disabled>Loading…</button>
      </section>
    );
  }

  return <>{children}</>;
};

export default connect(({ status }) => ({ status }))(LoadingStatus);

Now, we can update the reducer and listen for the event!

const status = (status = 'NOT_LOADED', action) => {
  if (action.type === FETCH_TWEETS) {
    return 'LOADING';
  }

  if (action.type === FETCH_TWEETS_FULFILLED) {
    return 'LOADED';
  }

  return status;
};

export default combineReducers({
  tweets,
  status,
});

Wrap the button in the <LoadingStatus> component again and we're good to go!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment