Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
  helpers
  middlewares
  state
  containers
  state
  README.md
  index.js
  package.json
  styles.dev.css
Size: Mime:
  README.md

@doodle/users-api-connector

Greenkeeper badge

Connects Doodle's users API to redux. For interactions with the API, this package provides a set of:

  • redux actions
  • redux reducers
  • react-saga sagas
  • redux inital state

that can be connected to the react-redux state through createStore.

💡 A connector is meant to provide a plug'n'play way to integrate data from a service API in your redux state.

As most Doodle frontends may contain some UI (like the user avatar in the header) related to the data for a logged-in user, this connector aims at to provide easily reusable state management for React frontends with redux state management.

Features

  • dispatch the loadUser() action to trigger cookie detection, token exchange and user profile fetching
  • Get an access token (provided that the user has auth cookies)
  • load user profile from /api/v2.0/users/me endpoint with token
  • provides user.data.profile and user.loading state
  • SSR capable

Demo

See web-example.

Installation

Add the private registry:

echo "registry=https://npm-proxy.fury.io/mfsTqYdDz3bsKFQJuMAR/tmf/" >> .npmrc

🚧 todo switch to nexus.doodle.com's npm registry

Add the package to your package.json:

yarn add --save --exact @doodle/users-api-connector

Documentation

Getting started

Let's get started with a minimal example which supports:

  • Server-side-rendering for universal react app with express, react-redux and react-saga
  • Client-side-rendering for react app with react-redux and react-saga
  • Call users service API through asynchronous sagas
mkdir connector-example
cd connector-example

npm init -y

echo "registry=https://npm-proxy.fury.io/mfsTqYdDz3bsKFQJuMAR/tmf/" >> .npmrc

npm i -S @doodle/users-api-connector redux redux-saga react react-redux react-dom express express-http-proxy webpack html-webpack-plugin babel-preset-react babel-loader babel-core babel-plugin-transform-object-rest-spread

Add a store.js file:

// store.js
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';

import createSagaMiddleware, { END } from 'redux-saga';

import { all, call } from 'redux-saga/effects';

import {
  createReducer as createUsersApiReducer,
  createState as createUsersApiInitialState,
  loadUserSaga,
} from '@doodle/users-api-connector';

const sagaMiddleware = createSagaMiddleware();

const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;

const middlewares = [
  sagaMiddleware,
  // ... other redux middlewares
];

const enhancers = composeEnhancers(
  applyMiddleware(...middlewares)
  // ... other enhancers
);

function* rootSaga(options = {}) {
  yield all([
    call(loadUserSaga, options.usersApi),
    // ... other sagas
  ]);
}

const rootReducer = combineReducers({
  ...createUsersApiReducer(),
  // ... other reducers
});

// load initial state through SSR or with the connector's local storage as a falback
const initialState = (typeof window !== 'undefined' && window.__SSR_STATE__) || {
  ...createUsersApiInitialState(),
  // ... other initial states
};

export default () => {
  const store = createStore(rootReducer, initialState, enhancers);
  store.runRootSaga = options => sagaMiddleware.run(rootSaga, options);
  store.close = () => store.dispatch(END);
  return store;
};

As an example, add a simple container in UserProfile.js, which is connected to the user state:

// UserProfile.js
import React from 'react';
import { connect } from 'react-redux';

const Profile = ({ name, loading }) => (
  <span style={{ fontStyle: loading ? 'italic' : 'normal' }}>{name ? `${name}${loading ? '...' : ''}` : 'Login'}</span>
);

const mapStateToProps = state => ({
  name: state.user.data.profile && state.user.data.profile.name,
  loading: state.user.loading,
});

export const UserProfile = connect(mapStateToProps)(Profile);

Client-side rendering

// client.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import createStore from './store';
import { UserProfile } from './UserProfile';

const store = createStore();

store.runRootSaga({
  usersApi: {
    url: '/api/v2.0/users', // using the API proxy below, avoiding CORS
  },
});

ReactDOM.render(
  <Provider store={store}>
    <UserProfile />
  </Provider>,
  document.getElementById('app')
);

Server-side rendering:

// server.js
import express from 'express';
import proxy from 'express-http-proxy';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import createStore from './store';
import { UserProfile } from './UserProfile';

// create express app
const app = express();

// simple html templating
const template = (html, state) => `<!doctype html>
<html>
<head><title>SSR</title></head>
<body>
<div id="app">${html}</div>
<script>
window.__SSR_STATE__ = ${JSON.stringify(state)};
</script>
<script src="/public/client-bundle.js" async></script>
</body>
</html>
`;

// API proxy, avoiding CORS
app.use(
  '/api/v2.0/users',
  proxy('beta.doodle.com', {
    https: true,
    proxyReqPathResolver: req => req.originalUrl,
  })
);

app.use('/public', express.static(`${process.cwd()}/dist/`));

// react server-side rendered pages
app.use('/', (req, res) => {
  const store = createStore();

  // kick off root saga: the @doodle/users-api-connector loadUserSaga bootstraps the `loadUser` action
  const saga = store.runRootSaga({
    usersApi: {
      getCookie: () => req.headers.cookie,
      url: 'https://beta.doodle.com/api/v2.0/users/',
    },
  });
  // dispatch the redux END action: current operations will resolve, watchers are terminated
  store.close();

  // after all pending sagas are terminated, render component with current store
  saga.done.then(() =>
    res.send(
      template(
        renderToString(
          <Provider store={store}>
            <UserProfile />
          </Provider>
        ),
        store.getState()
      )
    )
  );
});

// start app
app.listen(3000);
// webpack.config.js
const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');

const rules = [
  {
    test: /\.js/,
    loader: 'babel-loader',
    options: {
      presets: ['react'],
      plugins: ['transform-object-rest-spread'],
    },
  },
];
const plugins = [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify('production'),
    },
  }),
];
module.exports = [
  {
    entry: './client.js',
    output: {
      filename: 'client-bundle.js',
      path: `${__dirname}/dist`,
      publicPath: '/public/',
    },
    plugins: plugins,
    module: {
      rules: rules,
    },
  },
  {
    entry: './server.js',
    output: {
      filename: 'server-bundle.js',
      path: `${__dirname}/dist`,
    },
    target: 'node',
    externals: nodeExternals(),
    plugins: plugins,
    module: {
      rules: rules,
    },
  },
];

Compile & run

./node_modules/.bin/webpack
node ./dist/server-bundle.js &
open http://localhost:3000

Development

Login to private npm registry:

npm login
#Username: tmf
#Password:
#Email: (this IS public) tmf@doodle.com

After bumping version in package.json, publish new version:

npm publish

Creating link

  • run link command in the dist folder of the package you want to link
yarn link
  • run link command in the package folder you’d like to link
yarn link "@doodle/users-api-connector"
  • in case of similar error - Can't resolve 'react-dom' in ... - add an alias to webpack.config file:
resolve: {
    alias: {
      'react-redux': path.resolve('./node_modules/react-redux'),
      'react-dom': path.resolve('./node_modules/react-dom'),
    },
  },
  • after you're done run unlink command in the folder that was previously used to create a link:
yarn unlink

and:

yarn unlink "@doodle/users-api-connector"
yarn

to unlink a package that was symlinked during development in your project,

🔮 improvement switch to semantic versioning?

Publishing new version

  • make changes to the code
  • bump the package version (using the old version will not update the package in Gemfury)
  • make sure you have ONESKY_PUBLIC_KEY and ONESKY_SECRET_KEY in .env file
  • create a PR
  • merge PR to master branch (it will automatically publish your changes with the verions number you have specified in package.json)
  • you can check if everything went fine here: https://builder.internal.doodle-test.com/job/stack/job/users-connector/job/master/