I am building an app using redux and react-native.
I am curious about a pattern which I use. I have encountered no downsides however I haven't seen it in any tutorials which makes me wonder why nobody does it.
Instead of passing action creators as props in the connect function like
connect(mapStateToProps,{ func1, func2 })(Component);
I imported the app store inside of the module where I declare the functions in the first place:
import { AppStore } from '../App';
const actionCreator = () => {
doSomethng();
appStore.dispatch({ type: 'Action' });
};
This to me makes it easier to do async actions because I need no middleware:
import { AppStore } from '../App';
const actionCreator = async () => {
await doSomethng();
appStore.dispatch({ type: 'Action' });
};
I did this because of the js-lint error 'no-shadow'. It made me realise that in order to use it I had to import the action creators in the component file, and then pass it as a prop to the connect function in order for the action creator to have access to dispatch.
import { actionCreator1, actionCreator2 } from './actionCreators';
const myComponent = (props) => {
const { actionCreator1, actionCreator2 } = props; //shadowed names
return (
<Button onPress={actionCreator1} />
);
};
export default connect({}, { actionCreator1, actionCreator2 })(myComponent)
In my version I just import it once but do not pass it to connect. This eliminates the need to shadow names.
import { actionCreator1, actionCreator2 } from './actionCreators';
const myComponent = (props) => {
return (
<Button onPress={actionCreator1} />
);
};
export default connect({})(myComponent)
I like that you try to find your own solutions to your specific problems. It's the sign of an engineer, just in this case this isn't the solution.
I think the idea of how Redux teaches you to do things is not intended to be canonized. You have the ability to put a dispatcher on your props because it allows things to be transparent, meaning that things are bound outside of your class and injected in. You have hidden your store dependency by directly referencing it in some other files. It's no longer as obvious how your application works with regards to the workflow. Another react developer would be confused, I suppose that's the major downside.
If you're ok with those aspects what you're doing is fine. Fine as in, it gets the job done, but not "fine" in that it embraces concepts like Single Responsibility Principle
Related
I am creating a page in React. Lets say for eg. "Conatct us" page. This whole component must be reusable. So that other teams can use it as it is. This component will have its own redux store and api calls using axios.
What I want to confirm that if I export this "Contact Us" module as npm package, will it work fine for other teams? Why I am asking this is because other teams project will have their own redux store and axios instance. And I think we can have only one redux store in an app and maybe one axios interceptors (I may be wrong about axios though)
Could anyone help me out, what can be done in this case? One thing is sure that I will have to export this whole component as npm package.
I'm going to answer here to give you more details:
Let's say your component looks like this:
AboutUs:
import React, { Component } from "react";
import PropTypes from "prop-types";
export class AboutUs extends Component {
componentDidMount() {
const { fetchData } = this.props;
fetchData();
}
render() {
const { data, loading, error } = this.props;
if (loading) return <p>Loading</p>;
if (error) return <p>{error}</p>;
return (
// whatever you want to do with the data prop that comes from the fetch.
)
}
}
AboutUs.defaultProps = {
error: null,
};
// Here you declare what is needed for your component to work.
AboutUs.propTypes = {
error: PropTypes.string,
data: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
}),
fetchData: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
};
This component just takes a few props in order to work and the fetchData function will be a dispatch of any redux action.
So in one of the apps that are going to use the component library, assuming that they have their own store, you could do something like this.
In the component where you're planning to use the AboutUs component.
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
// this is the action that performs the data fetching flow.
import { fetchAboutUs } from "redux-modules/aboutUs/actions";
// The component that is above
import { AboutUs } from "your-component-library";
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
fetchData: fetchDashboard,
},
dispatch
);
};
const mapStateToProps = state => ({
loading: state.aboutUsReducer.loading,
error: state.aboutUsReducer.error,
data: state.aboutUsReducer.data,
});
const ReduxAboutUs = connect(
mapStateToProps,
mapDispatchToProps
)(AboutUs);
// Use your connected redux component in the app.
const SampleComponent = () => {
return <ReduxAboutUs />
}
This ensures that your component can work out of the box without redux, because you can explicitly use it without the redux dependency and just pass regular props and it will continue working. Also if you have different applications where you are going to use it you will have the control of which part of the store you want to use to inject the props for this component. Proptypes are quite useful here, because we're enforcing a few props in order let the devs what do we need to pass in order for the component to work properly.
some questions about React.js and Redux:
Can functional components also take advantage of the store and the states saved therein? e.g maybe in combination with React hooks like useEffect()?
In general, I can combine multiple reducers to one rootReducer and createStore(rootReducer) with it, and then pass it to a Provider Component that wraps my Component with it, this way, the store should be globally available in my whole app, correct?
For every component that want to use the store / states, do I always have to import the 2 methods mapStateToProps() and mapDispatchToProps() from react-redux for every Component and then connect them? Or can I also do this on some top-level component and make the usage of redux available in all my components globally, like in question 2) with the store provider?
last question: Can I still use the this.state property in my Components or use them in parallel as an addition (e.g for this Component isolated states) and then get the props from this state as usual with this.state.someState or is this not possible anymore when I already use Redux? And in the same way, can I still use / pass props to my components and read them from my Components as well, or is everything managed by state now only? (Or has the passing of props to my children nothing to do with Redux)?
1) Yes functional components can take advantage of the store. Its arguably much cleaner to read since props can be destructured right away.
const MyComponent = ({ auth }) => {
const [display, setDisplay] = useState(false)
useEffect(() => {
if(auth.user){
setDisplay(true)
}
}, [auth.user])
return(
<div>
{ display ? "Content": "Please sign in" }
</div>
)
}
const mapStateToProps = (state) => {
return{
auth: state.auth
}
}
export default connect(mapStateToProps)(MyComponent)
2) That is correct. You can also use combineReducers() which in some ways is cleaner to read.
import { createStore, combineReducers } from "redux"
import authReducer from "./reducers/authReducer"
import postReducer from "./reducers/postReducer"
const store = createStore(combineReducers({
auth: authReducer,
post: postReducer
}))
export default store
Then import store, wrap your App.js in a Provider and give it a prop of that store.
3) Generally, if you want your component to have direct access to the store it is a recognized pattern to use connect() in each one. Whether you decide to use mapStateToProps() or mapDispatchToProps() is entirely dependent on what that component needs to do. It does not required that you use both, you can just define one or the other in the connect().
import React, { useState } from "react"
import { addPost } from "/actions/postActions"
import { connect } from "react-redux"
const Form = ({ addPost }) => {
const [text, setText] = useState("")
const handleSubmit = (e) => {
e.preventDefault()
addPost(text)
}
return(
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)}/>
</form>
)
}
const mapDispatchToProps = (dispatch) => {
return {
addPost: (text) => dispatch(addPost(text))
}
}
export default connect(null, mapDispatchToProps)(Form)
4) You might have noticed by now that in the context of components, redux-state is stored as props. They are entirely different and isolated streams of data. So state remains untouched and controlled by the component itself. You can still freely use methods like this.state.dog even when your component is connected to the store. This is the isolation between component-state and redux-state.
import React, { useState } from "react"
import { connect } from "react-redux"
class MyDogs extends React.Component{
state = {
dog: "Tucker"
}
render(){
return(
<div>
Component State Value: {this.state.dog} //Tucker
Redux State Value: {this.props.dog} //Buddy
</div>
)
}
const mapStateToProps = (state) => {
return {
dog: state.dog
}
}
export default connect(mapStateToProps)(MyDogs)
When using many higher order functions I have found that the exports in a React project can get very messy. For example using react-redux, react-router, redux-form and react-i18next can look like:
const mapStateToProps = (state) => {
return {
// stuff
}
};
const form = {
// stuff
};
export default withRouter(withTranslation()(reduxForm(form)(connect(mapStateToProps)(Component)))));
This can get pretty unwieldy and keeping track of and changing the order can get messy. I created a simple one-liner to wrap the exports:
wrapper.js
export default ({ component, wrappers }) => wrappers.reduce((acc, wrapper) => wrapper(acc), component);
To be used like so:
import wrapper from '../path/to/wrapper.js';
/* rest of code */
const mapStateToProps = (state) => {
return {
// stuff
}
};
const form = {
// stuff
};
export default wrapper({
component: Component,
wrappers: [
// The order of the wrappers can be changed by just changing
// the order in which they appear here
connect(mapStateToProps),
reduxForm(form),
withTranslation,
withRouter,
],
});
So far this has worked, my question is: are there any potential issues with this approach that might not be immediately apparent? And are there any other ways or best practices for managing React exports?
I want to connect / bind actions in the same file in which I am defining my component, and ideally would like this component to be functional. The problem is doing this means I must alias my action so as to avoid the eslint rule no-shadow. You can see this in the code block below. However, I also use an IDE and aliasing these actions renders them invisible to my IDE when trying to find all usages of said actions.
Is there a way I can connect these dispatched actions to my functional component while making said actions visible to my IDE for debugging?
import React from 'react';
import {connect} from 'react-redux';
import {actionOne, actionTwo} from '../../../../actions';
const ComponentOne = ({actionOneDispatch, actionTwoDispatch}) => {
const handleClick = () => {
actionOneDispatch();
actionTwoDispatch()
};
return (
<button onClick={handleClick}>Click Me</button>
);
};
const mapDispatchToProps = (dispatch) => ({
actionOneDispatch: () => {
dispatch(actionOne());
},
actionTwoDispatch: () => {
dispatch(actionTwo());
},
});
export default connect(null, mapDispatchToProps)(ComponentOne);
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { setLocation } from 'redux/modules/filters'
import SearchForm from 'components/SearchForm/SearchForm'
type Props = {
};
export class HomeSearchContainer extends React.Component {
props: Props;
static contextTypes = {
router: React.PropTypes.object.isRequired
};
constructor(props) {
super(props)
this.onSearch = this.onSearch.bind(this)
}
onSearch(address, event) {
event.preventDefault()
if (address) {
this.props.actions.setLocation(address)
}
this.context.router.push('/browse_items')
}
render() {
return (
<SearchForm
onSearch={this.onSearch}
currentLocation={this.props.currentLocation}
/>
)
}
}
const mapStateToProps = (state) => {
return {
currentLocation: state.filters.location
}
}
const mapDispatchToProps = (dispatch) => {
var actions = {
setLocation
}
return {
actions: bindActionCreators(actions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(HomeSearchContainer)
I have a few questions to validate my understanding.
Do we ever re-use containers? Am I correct if I say we intend to re-use components but not containers?
In the above code, I want to create another container that doesn't redirect to /browse_items. In other words, I just want to override onSearch function of this container. Is it okay to extend this container?
First of all, in my mind a container is a certain kind of component, so following this article: https://medium.com/#dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.jqwffnfup I'll rather talk about container components and presentational components.
Ad 1) I would say we can re-use container components as well as presentational components - this always depends on how our code is structured. E.g. a container component might have child components, which can be different in different parts of the screen, but the container component is being re-used.
Ad 2) If you just want to have a different onSearch functionality, I might consider passing the onSearch callback to the HomeSearchContainer via the props. That way, you can re-use the same container, but it behaves differently.
Looking closely at your code, there is then not much left to the HomeSearchContainer, so you might as well use the SearchForm directly. If you need the SearchForm in multiple places, it might be worth pulling out the onSearch function into its own file and re-using that. But this is hard to judge without seeing the rest of the application. So I'm trying to throw some ideas at you here, which may or may not apply to your codebase.