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    
  containers
  state
  helpers
  state
  helpers
  containers
  state
  containers
  helpers
  middlewares
  state
  helpers
  styles.dev.css
  index.js
  package.json
  README.md
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 user profile fetching
  • 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/

Notes

Keycloak (since 3.0.0)

Since version 3.0.0, the Keycloak JWT is used for all API calls. In combination, the legacy access token (monolith access token) has been deprecated. Thus, in order to use this library, you need to populate the redux store (user.data.keycloakToken) with the Keycloak JWT before making any calls through this library.

Additionally, starting from version 3.0.0, the option to automatically trigger loadUser() on init has been removed. You need to manually dispatch the loadUser action to trigger it.

An example for the implementation of both of the above can be found in web-unified-dashboard.