After completing this guide, you'll have a new project with the following:
- React v16
- Redux
- Redux Form
- React-Router v4
- LocalStorage persistance
npx create-react-app <FOLDER_TO_CREATE_IN> --scripts-version=react-scripts-ts
npm i -S redux react-redux react-router-dom react-router-redux@next history redux-devtools-extension redux-form redux-persist
npm i -D @types/history @types/redux-form @types/react-router-dom @types/react-router-redux @types/react-redux
/** Libraries */
import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import createBrowserHistory from 'history/createBrowserHistory';
import { composeWithDevTools } from 'redux-devtools-extension';
import { persistStore, persistReducer } from 'redux-persist'; // <-- For persisting state in local storage
import storage from 'redux-persist/lib/storage'; // <-- For persisting state in local storage
/** Interfaces */
// This is the interface for your App. If you do not want to deal with that,
// you can replace `AppState` with `any`
// import { AppState } from "../types/"; <-- Import your AppState here
interface AppState {}
/** Components */
import reducers from '../reducers';
/** Initialisation */
const persistConfig = {
key: 'root',
whitelist: ['Account'], // <-- only Account state will persist across browser refreshes
storage,
};
const persistedReducer = persistReducer(persistConfig, reducers);
// Creating browser history
const history = createBrowserHistory();
// Building the middleware for intercepting and dispatching navigation actions
const middleware = routerMiddleware(history);
// Apply middleware and dev tools
const enhancers = composeWithDevTools(applyMiddleware(middleware));
const store = createStore<AppState>(persistedReducer, enhancers);
const persistor = persistStore(store);
export default store;
export { history, persistor };
Modify your index.tsx
file to include the store and history we just created above
/** Libraries */
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import { Route } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
/** Components */
import App from './components/App';
import store, { history, persistor } from './store/';
/** Styles */
import './index.css';
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ConnectedRouter history={history}>
<Route path="/" component={App} />
</ConnectedRouter>
</PersistGate>
</Provider>,
document.getElementById('root') as HTMLElement
);
Apart from your component reducers, we also need to add reducers for redux-form and react-router
/** Libraries */
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { reducer as formReducer } from 'redux-form';
/** Components */
// Import your component reducers here
/** Intefaces */
// import { AppState } from '../types'; <-- Import your AppState here
interface AppState {}
/**
* This is actual structure of your App's redux store.
* The keys in the object below is the keys you'll use
* to access it later in your components. You can add
* as many as you like in the format:
* [componentKey]:[componentReducer]
*
*/
export default combineReducers<AppState>({
// redux-form && react-router
router: routerReducer,
form: formReducer,
// component reducers
});
I like to specify an initial state for all my components. That way I have never have to worried about something not being defined at any point.
/**
* Helpers
*
* Since we not using ImmutableJS, we need to deep clone
* every time we need to make a change
*/
const deepClone = (objectToClone: object): FounderState =>
JSON.parse(JSON.stringify(objectToClone));
/** Initial State */
// Define your State interface and then the initial state
const initialState: {} = {};
export const componentReducer = (
state: ComponentState = initialState,
action: ActionInterface
): ComponentState => {
// Handle your actions here
// using deep clone like this :
switch (action.type) {
case 'SOME_ACTION_TYPE': {
//...handle action
const newState = deepClone(state);
newState.someProp = action.payload.someOtherProp;
return newState;
}
default:
return state;
}
};
Implementing actions should be faily straight forward at this point. Every different action type you write, it needs to be handled here.
- Add SASS
npm install --save node-sass-chokidar
- Add the following lines to scripts in
package.json
"scripts": {
+ "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/",
+ "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
- We need to watch the SASS files locally. No way to run two processes in parallel(yet) so we use this package
npm install --save npm-run-all
- Modify the following scripts in
package.json
"scripts": {
"build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/",
"watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive",
- "start": "react-scripts-ts start",
- "build": "react-scripts-ts build",
+ "start-js": "react-scripts-ts start",
+ "start": "npm-run-all -p watch-css start-js",
+ "build-js": "react-scripts-ts build",
+ "build": "npm-run-all build-css build-js",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject"
}
Once you've completed the above steps (including creating the reducers and actions as well), you are ready to run your project. Just execute npm start
to start a local server OR npm run build
to create a production build.