Listings Springer iX 8/2019, React State Management

Listing 1
...
import configureStore from './configureStore';

const store = configureStore();
export default class App extends React.Component<{}, State> {
  render() {
    return <Provider store={store}>
    ...
    </Provider>;
  }
}

------------------

Listing 2
import { Question } from '../Question';
export interface quizState {
  question: Question;
  finished: boolean;
  answered: null | number;
  count: {
    count: number;
    correct: number;
  };
}
const initialState = {
  question: {...},
  finished: false,
  answered: null,
  count: {
    count: 0,
    correct: 0,
  },
};
export default function(state: quizState = initialState, action: any) {
  return state;
}

------------------

Listing 3
import { combineReducers } from 'redux';
import quiz, { quizState } from './quiz/quiz.reducer';

export default combineReducers({
  quiz,
});

export interface State {
  quiz: quizState;
}

--------------------

Listing 4: Verbindung zwischen Redux- und React-Komponenten mit Container Components
import { connect } from 'react-redux';
import { State } from '../reducers';
import { getQuestion, getAnswered } from './quiz.selector';
import Quiz from './Quiz.component';

function mapStateToProps(state: State) {
  return {
    question: getQuestion(state),
    answered: getAnswered(state),
  };
}

export default connect(mapStateToProps)(Quiz);

--------------------

Listing 5
...
import { Dispatch } from 'redux';
import { answerQuestionAction } from './quiz.actions';

function mapStateToProps(state: State) {...}
function mapDispatchToProps(dispatch: Dispatch) {
  return {
    answerQuestion: (answer: number) => dispatch(answerQuestionAction(answer)),
  };
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Quiz);

--------------------

Listing 6
import update from 'immutability-helper';
import { Question } from '../Question';
import { ActionType } from 'typesafe-actions';
import { answerQuestionAction, ANSWER_QUESTION } from './quiz.actions';

export interface quizState {...}
const initialState = {...};

export default function(
  state: quizState = initialState,
  action: ActionType<typeof answerQuestionAction>
) {
  switch (action.type) {
    case ANSWER_QUESTION:
      return update(state, {
        answered: { $set: action.payload },
      });
    default:
      return state;
  }
}

--------------------

Listing 7: redux-observable in den Redux Store einbinden
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import rootEpic from './epics';
import { createEpicMiddleware } from 'redux-observable';

const epicMiddleware = createEpicMiddleware();

export default () => {
  const store = createStore(rootReducer, applyMiddleware(epicMiddleware));

  epicMiddleware.run(rootEpic);

  return store;
};

--------------------

Listing 8: Erweiterung der Actions
import { createStandardAction } from 'typesafe-actions';
import { Question } from '../Question';

export const ANSWER_QUESTION = 'ANSWER_QUESTION';
export const answerQuestionAction = createStandardAction(ANSWER_QUESTION)<
  number
>();

export const GET_QUESTION = 'GET_QUESTION';
export const getQuestionAction = createStandardAction(GET_QUESTION)<number>();

export const GET_QUESTION_SUCCESS = 'GET_QUESTION_SUCCESS';
export const getQuestionSuccessAction = createStandardAction(
  GET_QUESTION_SUCCESS
)<Question>();

--------------------

Listing 9: Bearbeitung der neuen Action im Epic
import { combineEpics, StateObservable, ofType } from 'redux-observable';
import { ActionType } from 'typesafe-actions';
import {
  getQuestionAction,
  GET_QUESTION,
  getQuestionSuccessAction,
} from './quiz.actions';
import { Observable, from } from 'rxjs';
import { mergeMap, map } from 'rxjs/operators';
import { Question } from '../Question';

const getQuestionEpic = (
  action$: Observable<ActionType<typeof getQuestionAction>>
) =>
  action$.pipe(
    ofType(GET_QUESTION),
    mergeMap(action => {
      const fetchPromise = fetch(`/question/${action.payload}`).then(response =>
        response.json()
      );
      return from(fetchPromise).pipe(
        map((question: Question) => getQuestionSuccessAction(question))
      );
    })
  );

export default combineEpics(getQuestionEpic);

--------------------

Listing 10
...
import {
  answerQuestionAction,
  ANSWER_QUESTION,
  GET_QUESTION_SUCCESS,
  getQuestionSuccessAction,
} from './quiz.actions';

export interface quizState {...}
const initialState = {...};
export default function(
  state: quizState = initialState,
  action: ActionType<
    typeof answerQuestionAction | typeof getQuestionSuccessAction
  >
) {
  switch (action.type) {
    case ANSWER_QUESTION:
      return update(...);
    case GET_QUESTION_SUCCESS:
      return update(state, {
        question: { $set: action.payload },
        answered: { $set: null },
      });
    default:
      return state;
  }
}

--------------------

Listing 11
...
import { answerQuestionAction, getQuestionAction } from './quiz.actions';

function mapStateToProps(state: State) {...}
function mapDispatchToProps(dispatch: Dispatch) {
  return {
    answerQuestion: (answer: number) => dispatch(answerQuestionAction(answer)),
    getQuestion: (id: number) => dispatch(getQuestionAction(id)),
  };
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Quiz);

--------------------

Listing 12
...
import { composeWithDevTools } from 'redux-devtools-extension';
...
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(epicMiddleware))
);
...

