10 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	Helpers
Strapi provides helpers so you don't have to develop again and again the same generic functions.
Request helper
A request helper is available to handle all requests inside a plugin.
It takes three arguments:
requestUrl: The url we want to fetch.options: Please refer to this documentation.true: This third argument is optional. If true is passed the response will be sent only if the server has restarted check out the example.
Usage
Path - /plugins/my-plugin/admin/src/containers/**/sagas.js.
import { call, fork, put, takeLatest } from 'redux-saga/effects';
// Our request helper
import request from 'utils/request';
import { dataFetchSucceeded, dataFetchError } from './actions';
import { DATA_FETCH } from './constants';
export function* fetchData(action) {
  try {
    const opts = {
      method: 'GET',
    };
    const requestUrl = `/my-plugin/${action.endPoint}`;
    const data = yield call(request, requestUrl, opts);
    yield put(dataFetchSucceeded(data));
  } catch(error) {
    yield put(dataFetchError(error))
  }
}
// Individual exports for testing
function* defaultSaga() {
  yield fork(takeLatest, DATA_FETCH, fetchData);
}
export default defaultSaga;
Simple example
Let's say that we have a container that fetches Content Type configurations depending on URL change.
Routing declaration:
Here we want to create a route /content-type/:contentTypeName for the ContentTypePage container.
Path — ./plugins/my-plugin/admin/src/container/App/index.js.
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import { Switch, Route, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { pluginId } from 'app';
import ContentTypePage from 'containers/ContentTypePage';
import styles from './styles.scss';
class App extends React.Component {
  render() {
    return (
      <div className={`${pluginId} ${styles.app}`}>
        <Switch>
          <Route exact path="/plugins/my-plugin/content-type/:contentTypeName" component={ContentTypePage} />
        </Switch>
      </div>
    );
  }
}
App.contextTypes = {
  router: PropTypes.object.isRequired,
};
export function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {},
    dispatch
  );
}
const mapStateToProps = createStructuredSelector({});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
export default compose(
  withConnect,
)(App);
Constants declaration:
Let's declare the needed constants to handle fetching data:
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/constants.js.
export const DATA_FETCH = 'myPlugin/ContentTypePage/DATA_FETCH';
export const DATA_FETCH_ERROR = 'myPlugin/ContentTypePage/DATA_FETCH_ERROR';
export const DATA_FETCH_SUCCEEDED = 'myPlugin/ContentTypePage/DATA_FETCH_SUCCEEDED';
Actions declaration:
Let's declare our actions.
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/actions.js.
import {
  DATA_FETCH,
  DATA_FETCH_ERROR,
  DATA_FETCH_SUCCEEDED,
} from './constants';
export function dataFetch(contentTypeName) {
  return {
    type: DATA_FETCH,
    contentTypeName,
  };
}
export function dataFetchError(errorMessage) {
  return {
    type: DATA_FETCH_ERROR,
    errorMessage,
  };
}
export function dataFetchSucceeded(data) {
  // data will look like { data: { name: 'User', description: 'Some description' } }
  return {
    type: DATA_FETCH_SUCCEEDED,
    data,
  };
}
Reducer setup:
Please refer to the Immutable documentation for informations about data structure.
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/reducer.js.
import { fromJS, Map } from 'immutable';
import {
  DATA_FETCH,
  DATA_FETCH_ERROR,
  DATA_FETCH_SUCCEEDED
} from './constants';
const initialState = fromJS({
  contentTypeName,
  error: false,
  errorMessage: '',
  data: Map({}),
});
function contentTypePageReducer(state = initialState, action) {
  switch (action.type) {
    case DATA_FETCH:
      return state.set('contentTypeName', action.contentTypeName);
    case DATA_FETCH_ERROR:
      return state
        .set('error', true)
        .set('errorMessage', action.errorMessage);
    case DATA_FETCH_SUCCEEDED:
      return state
        .set('error', false)
        .set('data', Map(action.data.data));
    default:
      return state;
  }
}
export default contentTypePageReducer;
Selectors setup:
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/selectors.js.
import { createSelector } from 'reselect';
/**
 * Direct selector to the contentTypePage state domain
 */
const selectContentTypePageDomain = () => state => state.get('contentTypePage');
/**
 * Other specific selectors
 */
/**
 * Default selector used by ContentTypePage
 */
const selectContentTypePage = () => createSelector(
  selectContentTypePageDomain(),
  (substate) => substate.toJS()
);
const makeSelectContentTypeName = () => createSelector(
  selectContentTypePageDomain(),
  (substate) => substate.get('contentTypeName');
)
export default selectContentTypePage;
export { makeSelectContentTypeName, selectContentTypePageDomain };
Handling route change:
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/index.js.
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { bindActionCreators, compose } from 'redux';
import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import { map } from 'lodash';
// Utils to create the container's store
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
import { dataFetch } from './actions';
import { selectContentTypePage } from './selectors';
import saga from './sagas';
import reducer from './reducer';
import styles from './styles.scss';
export class ContentTypePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
  constructor(props) {
    super(props);
    this.links = [
      {
        to: 'plugin/my-plugin/content-type/product',
        info: 'Product',
      },
      {
        to: 'plugin/my-plugin/content-type/user',
        info: 'User',
      },
    ];
  }
  componentDidMount() {
    this.props.dataFetch(this.props.match.params.contentTypeName);
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.match.params.contentTypeName !== this.props.match.params.contentTypeName) {
      this.props.dataFetch(nextProps.match.params.contentTypeName);
    }
  }
  render() {
    return (
      <div className={styles.contentTypePage}>
        <div>
          <ul>
            {map(this.links, (link, key) => (
              <li key={key}>
                <NavLink to={link.to}>{link.info}</NavLink>
              </li>
            ))}
          </ul>
        </div>
        <div>
          <h1>{this.props.data.name}</h1>
          <p>{this.props.data.description}</p>
        </div>
      </div>
    );
  }
}
const mapStateToProps = selectContentTypePage();
function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {
      dataFetch,
    },
    dispatch,
  );
}
ContentTypePage.propTypes = {
  data: PropTypes.object.isRequired,
  dataFetch: PropTypes.func.isRequired,
  match: PropTypes.object.isRequired,
};
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withSaga = injectSaga({ key: 'contentTypePage', saga });
const withReducer = injectReducer({ key: 'contentTypePage', reducer });
export default compose(
  withReducer,
  withSaga,
  withConnect,
)(ContentTypePage);
Fetching data:
The sagas.js file is in charge of fetching data.
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/sagas.js.
import { LOCATION_CHANGE } from 'react-router-redux';
import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
import request from 'utils/request';
import {
  dataFetchError,
  dataFetchSucceeded,
} from './actions';
import { DATA_FETCH } from './constants';
import { makeSelectContentTypeName } from './selectors';
export function* fetchData() {
  try {
    const opts = { method: 'GET' };
    // To make a POST request { method: 'POST', body: {Object} }
    const endPoint = yield select(makeSelectContentTypeName());
    const requestUrl = `my-plugin/**/${endPoint}`;
    // Fetching data with our request helper
    const data = yield call(request, requestUrl, opts);
    yield put(dataFetchSucceeded(data));
  } catch(error) {
    yield put(dataFetchError(error.message));
  }
}
function* defaultSaga() {
  const loadDataWatcher = yield fork(takeLatest, DATA_FETCH, fetchData);
  yield take(LOCATION_CHANGE);
  yield cancel(loadDataWatcher);
}
export default defaultSaga;
Example with server autoReload watcher
Let's say that you want to develop a plugin that needs server restart on file change (like the settings-manager plugin) and you want to be aware of that to display some stuff..., you just have to send a third argument: true to our request helper and it will ping a dedicated route and send the response when the server has restarted.
Path — ./plugins/my-plugin/admin/src/containers/**/sagas.js.
import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
import request from 'utils/request';
import {
  submitSucceeded,
  submitError,
} from './actions';
import { SUBMIT } from './constants';
// Other useful imports like selectors...
// ...
export function* postData() {
  try {
    const body = { data: 'someData' };
    const opts = { method: 'POST', body };
    const requestUrl = `**yourUrl**`;
    const response = yield call(request, requestUrl, opts, true);
    if (response.ok) {
      yield put(submitSucceeded());      
    } else {
      yield put(submitError('An error occurred'));
    }
  } catch(error) {
    yield put(submitError(error.message));
  }
}
function* defaultSaga() {
  yield fork(takeLatest, SUBMIT, postData);
  // ...
}
export default defaultSaga;