Problem: IDE does not resolve props passed to the component via connect()
Note: this is not a bug, but an inconvenience to the coder
Say I have this React component connected to Redux via connect():
class SomeComponent extends Component {
render() {
return (
{this.props.someObject ? this.props.someObject : ''}
);
}
}
function mapStateToProps(state) {
return {
someObject: new SomeObject(state.someReducer.someObjectInfo),
};
}
function mapDispatchToProps(dispatch) {
return {
// ...
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatsList);
I'm using the IntelliJ IDE, and any prop connected to the component in the above manner, such as someObject, will get an unresolved variable warning. And if someObject has some properties/methods, they will neither be resolved nor show up in code suggestions (which are really helpful).
A workaround
Pass state and dispatch themselves as props:
function mapStateToProps(state) {return {state};}
function mapDispatchToProps(dispatch) {return {dispatch};}
Define my variables in the constructor (as opposed to via props):
constructor(props) {
this.someVar = props.state.someReducer.someVar;
this.someObj = new SomeObject(props.state.someReducer.someObjectInfo;
}
Update the variables manually whenever props change:
componentWillReceiveProps(nextProps) {
someObject.update(nextProps.state.someReducer.someObjectInfo);
}
The drawback is having additional boilerplate logic in componentWillReceiveProps, but now the IDE happily resolves the variables and code suggestion works.
Question
Is the workaround preferable? I'm using it, like it so far, and have not observed any other drawbacks thus far. Is there a better way to get the IDE to understand the code?
Motivation (verbose; only for those interested in why I want to accomplish the above)
The Redux tutorials show a simple way to connect state/dispatch to props, e.g.:
function mapStateToProps(state) {
users: state.usersReducer.users
chats: state.chatsReducer.chats
}
function mapDispatchToProps(dispatch) {
addUser: (id) => dispatch(usersActions.addUser(id))
addChatMsg: (id, msg) => dispatch(chatsActions.addChatMsg(id, msg)
}
In the example above, the coder of a component will need to know every relevant reducers' names and their state variables. This can get messy for the coder. Instead, I want to abstract these details away from the component. One way is with a "module" class that accepts state and dispatch, and provides all get/set methods:
class Chats {
// Actions
static ADD_MESSAGE = "CHATS/ADD_MESSAGE";
constructor(globalState, dispatch) {
this.chatsState = globalState.chats;
this.dispatch = dispatch;
}
// Get method
getChats() {
return this.chatsState.chats;
}
// Set method
addChatMessage(id, msg) {
return this.dispatch({
type: Chats.ADD_MESSAGE,
id,
msg
};
}
// Called by componentWillReceiveProps to update this object
updateChats(nextGlobalState) {
this.chatsState = nextGlobalState.chats;
}
}
Now, if a Component requires the Chats module, a coder simply does this:
class SomeComponent extends Component {
constructor(props) {
this.chats = new Chats(props.state, props.dispatch);
}
componentWillReceiveProps(nextProps) {
this.chats.updateChats(nextProps);
}
// ...
}
And now, all Chats get/set methods and properties will be available, and will be picked up by the IDE.
I think newest Idea can now understand component properties defined via propTypes and provides code completion for them. So you just declare propTypes. And it is not even a workaround, it's a good practice in my opinion.
class ChatsList extends Component {
static propTypes = {
someObject: PropTypes.shape({
color: PropTypes.string,
someFunc: PropTypes.func
}),
someDispatcher: PropTypes.func
};
render() {
return (
{this.props.someObject ? this.props.someObject : ''}
);
}
}
function mapStateToProps(state) {
return {
someObject: new SomeObject(state.someReducer.someObjectInfo),
};
}
function mapDispatchToProps(dispatch) {
return {
someDispatcher: Actions.someDispatcher
// ...
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatsList);
Also, passing the entire state is a bad idea, since a component will receive props and get re-renderend if anything changes in the entire state (unless you provide shouldComponentUpdate)
Related
In my react-redux application, when I mount 2 components together, state of first is being over-ridden by second.
In my first component, I have following state and actions defined:
function mapStateToProps(state) {
return {
comp1: state.comp1,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: {
action1: bindActionCreators(action1, dispatch),
action2: bindActionCreators(action2, dispatch),
}
};
}
And the second component looks something like this:
function mapStateToProps(state) {
return {
comp2: state.comp2,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: {
action3: bindActionCreators(action3, dispatch),
action4: bindActionCreators(action4, dispatch),
}
};
}
But when both the components are finally mounted, only second components state and actions remain. And 1st component's state and action become undefined.
I simply render the components this way:
<Comp1/>
<Comp2/>
Redux version: 3.7.2
React-redux version: 5.0.6
Any help is appreciated. Thanks in advance!
I have this part of my code:
constructor(props) {
super(props)
this.state = {
symbol: '',
side: '',
status: ''
};
this.onInputChange = this.onInputChange.bind(this);
this.onValueChangeSide = this.onValueChangeSide.bind(this);
this.onValueChangeStatus = this.onValueChangeStatus.bind(this);
this.onFormSelect = this.onFormSelect.bind(this);
}
onInputChange(event) {
this.setState({ symbol: event.target.value });
}
onValueChangeSide(event) {
this.setState({ side: event.target.value});
}
onValueChangeStatus(event) {
this.setState({ status: event.target.value});
}
onFormSelect(event) {
this.props.requestAccountsFilter(this.state.symbol, this.state.side,
this.state.status);
}
The requestAccountsFilter is an Action. Its code is:
export function requestAccountsFilter(symbol, side, status) {
return {
type: ACCOUNT_FILTERS,
payload: {
symbol,
side,
status
}
};
}
That approach works fine.
Furthermore, i want to make my component Stateless so i create a container. My problem is that i don't know how to dispatch my action with the above functionality.
I write this:
const MapDispatchToProps = dispatch => (
{
requestAccountsFilter: (symbol, side, status) => {
dispatch(requestAccountsFilter(symbol, side, status));
}
}
);
but it didn't work.
How to dispatch my action in the MapDispatchToProps??
The purpose of mapDispatchToProps is not to actually dispatch actions directly.
mapDispatchToProps is used to bind action creators with dispatch and pass these new bound functions as props to the component.
The main benefit of using mapDispatchToProps is that it makes our code cleaner by abstracting away the store's dispatch method from components. Therefore we can call props that are functions without acces to dispatch like so
this.props.onTodoClick(id);
Whereas if we didn't use mapDispatchToProps then we would have to pass dispatch separately to components and dispatch actions like so:
this.props.dispatch(toggleTodo(id));
You would use mapDispatchToProps as shown in your example code, and then elsewhere write:
mapDispatchToProps must be a pure function and cannot have side effects.
Dispatching actions from inside the function would be considered a side effect. A function has one or more 'side effects' when the evaluation of the function changes state outside of itself (changes global state of app)
Instead use lifecycle hooks to dispatch actions in response to prop changes on components:
class exampleComponent extends Component {
componentDidMount() {
this.props.fetchData(this.props.id)
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.props.fetchData(this.props.id)
}
}
// ...
}
I've got a component that builds search/sort filters that can be selected. I want the selected state of those filters to be tracked in redux so that the search builder can subscribe and see when they change and update appropriately. the thing I'm trying to figure out how to do (in a way that doesn't feel weird) is populate the filter objects into the state. Eg, right now in the <Search /> component I have something like:
<OptionPicker
group={'searchFilters'}
options={{word: 'price', active: true},
{word: 'distance', active: false},
{word: 'clowns', active: false}}
/>
So how to get those props into state to be used without triggering multiple element renders. I'm also rendering the app on the server as well, so for the initial attachment render, the state already has the options.
In the OptionPicker component I've got:
class OptionPicker extends Component {
constructor(props) {
super(props)
if (!props.optionstate) {
this.props.addOptionState(props)
}
}
render() {
return {this.props.optionstate.word.map((word) => <Option ... />)}
}
}
function mapStateToProps(state, props) {
return {
optionstate: state.optionstate[props.group],
};
}
function mapDispatchToProps(dispatch) {
return {
addOptionState: (props) => {
dispatch(addOptionState(props));
},
optionToggled: (group, word) => {
dispatch(updateOptionState(group, word));
}
};
}
export default connect(mapStateToProps,mapDispatchToProps)(OptionGroup);
This kinda works, but there exists a time when render is called before the redux state has been populated, which throws an error. I could guard against that, but none of this feels "right". Shouldn't that prop always be there? Is there a pattern for this that I'm missing?
I agree with you in that the prop should always be there. The pattern I use for this is to set up an initial state and to pass it to the reducer function:
export const INITIAL_STATE = {
optionstate: { /* all filters are present but deactivated */ }
};
export default function (state = INITIAL_STATE, action) {
// reduce new actions into the state
};
I can change the states and update the UI by using Redux. But how to show injected props by Redux on console by using console like console.log(this.props) in run-time. I cannot. I've never seen the props.
Is there a way to show component (class) props -that are assigned from Redux store like the code below-?
function mapStateToProps(state) {
return { iconSize: state.iconSize }
}
function mapDispatchToProps(dispatch) {
return {
setIconSize: (size) => dispatch(setIconSize(size))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Main)
In this example you can rewrite mapStateToProps function:
function mapStateToProps(state) {
const props = { iconSize: state.iconSize };
console.log(props);
return props;
}
It will log props each time this function is invoked.
Preface: I'm new to Meteor, Angular, and Typescript, so there is a very real possibility of an XY problem somewhere in here.
I'm working on a simple project management app using Meteor and Angular 2 (using the angular2-meteor package) where the structure (for now) consists of projects which have events. One view is a list of projects. Clicking on a project shows a modal of the project's details, including a list of the project's events. So, three components: ProjectList, ProjectDetails, and ProjectEventsList. ProjectDetails uses a Session variable to know which project to show, and that works. However, the list of events in the modal doesn't update after it is created for the first project clicked on.
ProjectEventsList.ts
import {Component, View} from 'angular2/core';
import {MeteorComponent} from 'angular2-meteor';
import {ProjectEvents} from 'collections/ProjectEvents';
#Component({
selector: 'projectEventsList',
inputs: ['projectId']
})
#View({
templateUrl: '/client/projectEventsList/projectEventsList.html'
})
export class ProjectEventsList extends MeteorComponent {
projectEvents: Mongo.Cursor<ProjectEvent>;
projectId: string;
constructor() {
super();
this.subscribe('projectEvents', this.projectId, () => {
this.autorun(() => {
this.projectEvents = ProjectEvents.find({projectId: this.projectId});
}, true);
});
}
}
As I understand it (though I may be way off here), I'm having difficulty getting autorun to, well, automatically run. I've tried putting a getter and setter on projectId and it does get updated when I click on a project, but the code inside autorun doesn't run after the first click. Things I've tried:
Switching the nesting of subscribe() and autorun().
Adding/removing the autobind argument to both subscribe() and autorun(). I don't really understand what that's supposed to be doing.
Moving the subscribe code to a setter on projectId:
private _projectId: string = '';
get projectId() {
return this._projectId;
}
set projectId(id: string) {
this._projectId = id;
this.subscribe('projectEvents', this._projectId, () => {
this.projectEvents = ProjectEvents.find({projectId: this._projectId});
}, true);
}
When I do this the list stops displaying any items.
If this all seems like it should work, I'll create a small test case to post, but I am hoping that something in here will be obviously wrong to those who know. Thanks!
this.subscribe() and this.autorun() doesn't seem to be part of the Angular component class. If this is an external library you might need to explicitly run it in an Angular zone for change detection to work:
constructor(private zone: NgZone) {
this.subscribe('projectEvents', this.projectId, () => {
this.autorun(() => {
zone.run(() => {
this.projectEvents = ProjectEvents.find({projectId: this.projectId});
});
}, true);
});
}
If you want to subscribe to events fired from the component itself use host-binding
#Component(
{selector: 'some-selector',
host: {'projectEvents': 'projectsEventHandler($event)'}
export class SomeComponent {
projectsEventHandler(event) {
// do something
}
}
I eventually got the setter method working, as shown below. It feels clunky, so I'm hoping there's a cleaner way to do this, but the below is working for me now (i.e., the list of events is updated when the parent component (ProjectList) sends a new projectId to the input.
ProjectEventsList.ts
import {Component, View} from 'angular2/core';
import {MeteorComponent} from 'angular2-meteor';
import {ProjectEvents} from 'collections/ProjectEvents';
#Component({
selector: 'projectEventsList',
inputs: ['projectId']
})
#View({
templateUrl: '/client/projectEventsList/projectEventsList.html'
})
export class ProjectEventsList extends MeteorComponent {
projectEvents: Mongo.Cursor<ProjectEvent>;
set projectId(id: string) {
this._projectId = id;
this.projectEventsSub = this.subscribe('projectEvents', this._projectId, () => {
this.projectEvents = ProjectEvents.find({projectId: this._projectId}, {sort: { startDate: 1 }});
}, true);
}
get projectId() {
return this._projectId;
}
constructor() {
super();
this.subscribe('projectEvents', this.projectId, () => {
this.projectEvents = ProjectEvents.find({projectId: this.projectId});
}, true);
}
}