I'm fairly new to React/Redux, and I'm very very amazed with all of the amazing work you guys have all done. So thank you so much for the time you've put into this project.
I come from backend development and one very common pattern are IoC containers for configuring your app services at runtime to be used. One of the first things i noticed is that there is a lack of maybe documentation or even support for IoC with React/Redux. I wanted to create this issue as a discussion reviewing the different benefits/drawbacks and methods of implementing IoC within React/Redux apps.
The most common use case for IoC on the frontend for me is configuring an API client to interact with the backend API.
function ApiClient(axios) {
return {
createUser: (user) => {
return axios.post('/users', user);
}
};
}
function MockApiClient(res) {
return {
createUser: (user) => {
return Promise.resolve(res);
}
}
}
function createAppActions(apiClient) {
return {
createUser: (name) => {
return (dispatch, getState) => {
dispatch({type: 'CREATE_USER_START', name: name});
apiClient.createUser({name: name}).then((res) => {
dispatch({type: 'CREATE_USER_END', res: res});
})
}
}
}
}
const appActions = createAppActions(new ApiClient(axios));
const mockAppActions = createAppActions(new MockApiClient({id: 1}));
// in a test somewhere
dispatch(mockAppActions.createUser('Test'));
// we can now subscribe and verify the CREATE_USER_START and CREATE_USER_END were fired with the expected name and result.
Now typically in my apps, i'll use bottlejs to configure my services and actions, and then in my mapDispatchToProps, I access my global container to retrieve the configured actions which I can then be used in my components.
import {bindActionCreators} from 'redux';
import configuredBottle from './ioc'; // exports a configured service container
connect(null, (dispatch) => {
return bindActionCreators(configuredBottle.container.appActions, dispatch);
})(SomeComponent);
Now, I know there are other ways of going about this, but I was curious on if there is a more idiomatic way to implement IoC in react/redux apps.
A few ways other ways to configure this would be to just have a specific js file used for creating the configured services in my app instead of using the container to inject the deps, but this would stubbing/mocking out the JS files with jest or something to implement testing, and I think it's overall a bit less flexible than using an IoC container, however it would get the job down.
I've also considered making a custom mapDispatchToPropsFactory that will inject a configured bottleJs container as a third argument, but not totally sure it's worth going down that route if IoC isn't something I should be even focusing on.
I don't think we're going to be very prescriptive about how you choose to use our APIs. The base concept of a predictable state container is fairly novel and plugs into a lot of architectures. How that's done is really up to you.
If you're specifically concerned about testing, we have a lot of good documentation there: https://redux.js.org/docs/recipes/WritingTests.html And also our FAQ has expanded quite a bit recently: https://redux.js.org/docs/FAQ.html And I'd be remiss to not point out the Structuring Reducers docs, which can help with designing your actions as well: https://redux.js.org/docs/recipes/StructuringReducers.html
@ragboyjr : what I'd generally recommend is writing all logic as separate thunk action creators (or as using other side effect approaches, such as sagas and observables). From there, redux-thunk supports injecting an extra argument into thunk functions:
// configureStore.js
import thunkMiddleware from "redux-thunk";
import myApiClient from "./myApiClient";
const thunkWithMyApiInjected = thunkMiddleware.withExtraArgument({api : myApiClient});
const middlewareEnhancer = applyMiddleware(thunkWithMyApiInjected);
export default createStore(rootReducer, middlewareEnhancer);
// later
function thunkThatMakesApiCalls() {
return (dispatch, getState, {api}) => }
// use injected API object
}
}
Most helpful comment
@ragboyjr : what I'd generally recommend is writing all logic as separate thunk action creators (or as using other side effect approaches, such as sagas and observables). From there,
redux-thunksupports injecting an extra argument into thunk functions: