I want to create a selector with memoization using reselect based on some ownProps of mapStateToProps.
You can do this by connecting the selector to a component using the connect method provided by react-redux, then passing the component props (ownProps) as the second argument to the selector.
container.js
import { connect } from 'react-redux';
import { getVisibleTodos } from './selectors';
...
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props),
};
};
const VisibleTodoList = connect(
mapStateToProps,
)(TodoList);
export default VisibleTodoList;
You can then access those props in your selector
selectors.js
import { createSelector } from 'reselect';
const getVisibilityFilter = (state, props) =>
state.todoLists[props.listId].visibilityFilter;
const getTodos = (state, props) =>
state.todoLists[props.listId].todos;
const getVisibleTodos = createSelector(
...
);
export default getVisibleTodos;
However, this will not memoize correctly if you have multiple instances of the component you're passing props from. In that case, the selector would receive a different props argument each time, so it would always recompute instead of returning a cached value.
To share a selector across multiple components while passing in props and retaining memoization, each instance of the component needs its own private copy of the selector.
You can do this by creating a function that returns a new copy of the selector each time it's called.
selectors.js
import { createSelector } from 'reselect';
const getVisibilityFilter = (state, props) =>
state.todoLists[props.listId].visibilityFilter;
const getTodos = (state, props) =>
state.todoLists[props.listId].todos;
const makeGetVisibleTodos = () => {
return createSelector(
...
);
}
export default makeGetVisibleTodos;
If the mapStateToProps argument supplied to connect returns a function instead of an object, it will be used to create an individual mapStateToProps function for each instance of the container.
With that in mind, you can create a function makeMapStateToProps that creates a new getVisibleTodos selector, and returns a mapStateToProps function that has exclusive access to the new selector:
import { connect } from 'react-redux';
import { makeGetVisibleTodos } from './selectors';
...
const makeMapStateToProps = () => {
const getVisibleTodos = makeGetVisibleTodos();
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props),
};
};
return mapStateToProps;
};
const VisibleTodoList = connect(
makeMapStateToProps,
)(TodoList);
export default VisibleTodoList;
Now each instance of the VisibleTodosList container will get its own mapStateToProps function with a private getVisibleTodos selector. Memoization will now work correctly regardless of the render order of the containers.
This was adapted (blatently copied) from the Reselect documentation
Related
I've got a stateless component that has two redux props: one state prop, and one dispatch prop.
It renders the state prop and fires off the dispatch prop, which makes an api call to populate the state prop.
const Component = ({ items, getItems }) => {
React.useEffect(() => {
getItems()
}, [])
return (
<div>
{items.map(item => <li>{item}</li>)}
</div>
)
}
When the component first renders, items is empty and it renders as empty. But I see the action finishing and updating the redux store. However, the component does not rerender and remains empty.
However, if I unmount the component and remount it (in my case by navigating to a different route using React Router), the items get updated and rendered.
Shouldn't the component rerender when the redux store updates the items prop?
Edit:
I've added the container component as well. I'm using typescript if that makes a difference which I assume it doesn't. And as for rendering, I just render the container component somewhere
import { connect } from 'react-redux'
import Component from 'components/dashboard/item/Component'
import { getItems } from 'actions/item'
import { ItemByIdType, RootState } from 'types'
interface StateProps {
items: ItemByIdType
}
interface DispatchProps {
getItems: () => any,
}
interface OwnProps {}
export interface IComponent extends StateProps, DispatchProps, OwnProps {}
const mapStateToProps: (state: RootState) => StateProps = (state: RootState) => {
return {
items: state.item.byId
}
}
const mapDispatchToProps: (dispatch: any) => DispatchProps = (dispatch: any) => {
return {
getItems: () => dispatch(getItems()),
}
}
const ComponentContainer = connect<StateProps, DispatchProps, OwnProps>(
mapStateToProps,
mapDispatchToProps
)(Component)
export default ComponentContainer
A module I have contains the following line. connect seems to have two pairs of parentheses. What does this mean?
export default connect(mapStatetoProps, mapDispatchToProps).
(LandingComponent)
Tried to lookup various documentation
import { connect } from 'react-redux'
import { LandingComponent } from './Landing'
const mapStatetoProps = state => {
return {}
}
const mapDispatchToProps = dispatch => {
return {}
}
export default connect(mapStatetoProps, mapDispatchToProps)
(LandingComponent)
Expect to understand what the syntax means.
The second set of parentheses is because connect(..., ...) returns a function. This function is a component decorator which is why it is called with the landing component class.
If you split it up it might become clearer:
const decorate = connect(mapStatetoProps, mapDispatchToProps);
const ReduxConnectedLandingComponent = decorate(LandingComponent);
export default ReduxConnectedLandingComponent;
In this case decorate is a function that accepts a single component and returns a component. i.e. it takes the plain component and returns a smarter one which pulls props from the nearest provided store in the hierarchy.
Example:
const TodoItem = ({ todo, destroyTodo }) => {
return (
<div>
{todo.text}
<span onClick={destroyTodo}> x </span>
</div>
)
}
const mapStateToProps = state => {
return {
todo: state.todos[0]
}
}
const mapDispatchToProps = dispatch => {
return {
destroyTodo: () =>
dispatch({
type: 'DESTROY_TODO'
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoItem)
mapStateToProps and mapDispatchToProps are both pure functions that are provided the stores “state” and “dispatch” respectively. Furthermore, both functions have to return an object, whose keys will then be passed on as the props of the component they are connected to.
In this case, mapStateToProps returns an object with only one key : “todo”, and mapDispatchToProps returns an object with the destroyTodo key.
The connected component (which is exported) provides todo and destroyTodo as props to TodoItem.
Source: https://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/
I have a store with 9 reducers in it. I only want my component to listen to one of them discoverSearch. Using shorthand version of mapStateToProps this is my code. However, the component is still being delivered all reducers in componentWillReceiveProps.
Component
import React from 'react'
import { connect } from 'react-redux'
import { View, Text, Animated, Dimensions } from 'react-native'
const _ = require('lodash')
import colors from '../../Color'
import DiscoverSearchResultChannel from './DiscoverSearchResultChannel'
import DiscoverSearchResultEpisode from './DiscoverSearchResultEpisode'
const { height, width } = Dimensions.get('window')
class DiscoverSearchResultsContainer extends React.Component {
constructor() {
super()
this.generateResultsList = this.generateResultsList.bind(this)
}
generateResultsList(results, type) {
const components = []
for (let i = results.length - 1; i >= 0; i--) {
if (type === 'CHANNEL') {
const result =
(<DiscoverSearchResultChannel
entry={results[i]}
key={`dsearch-${results[i].id}`}
navigation={this.props.navigation}
/>)
components.push(result)
} else if (type === 'EPISODE') {
const result =
(<DiscoverSearchResultEpisode
entry={results[i]}
key={`dsearch-${results[i].id}`}
navigation={this.props.navigation}
/>)
components.push(result)
}
}
return components
}
render() {
const { episodes, channels } = this.props.discoverSearch.results
return (
<Animated.ScrollView
style={styles.shell}
contentContainerStyle={styles.innerContent}
>
<Text style={styles.divider}>Podcasts</Text>
{
_.isUndefined(channels) ? null : this.generateResultsList(channels, 'CHANNEL')
}
<Text style={styles.divider}>Episodes</Text>
{
_.isUndefined(episodes) ? null : this.generateResultsList(episodes, 'EPISODE')
}
</Animated.ScrollView>
)
}
}
export default connect(store => (
{ discoverSearch: store.discoverSearch },
dispatch => dispatch
))(DiscoverSearchResultsContainer)
Store
import { applyMiddleware, createStore } from 'redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'
import promise from 'redux-promise-middleware'
import combinedReducers from './reducers/CombineReducers'
const middleware = applyMiddleware(
thunk,
createLogger(),
promise()
)
export default createStore(combinedReducers, middleware)
DiscoverSearchReducer
const initialState = {
results: []
}
const DiscoverSearchReducer = (state = initialState, action) => {
let newState
switch (action.type) {
case 'DISCOVER_SEARCH_REQUEST_OUT':
// TODO
break
case 'DISCOVER_SEARCH_RETURN':
newState = {
...state,
results: action.payload
}
break
default:
return state
}
return newState
}
Reducers
export default combineReducers({
network: NetworkReducer,
audioPlayer: AudioPlayerReducer,
fileHandler: FileHandlerReducer,
currentTrack: CurrentTrackReducer,
trackQueue: TrackQueueReducer,
asyncStatus: AsyncStatusReducer,
makeClip: MakeClipReducer,
userProfile: UserProfileReducer,
scrollListener: ScrollListenReducer,
userClips: UserClipsReducer,
discoverSearch: DiscoverSearchReducer,
})
App Entry
class App extends React.Component {
componentWillMount() {
// TODO
// Initialize Firebase => get UID then...
store.dispatch(fetchUser('7713BNBNPODPIE'))
}
componentDidMount() {
TrackPlayer()
}
render() {
return (
<Provider store={store} >
<View style={{ flex: 1 }}>
<Navigation />
<BottomPlayer />
</View>
</Provider>
)
}
}
export default App
The connect piece of Redux is a little new to me so I might be missing something obvious?
Edits:
Added App.js entry point
Added full DiscoverSearchResultsContainer component minus styles
The error is in your connect function:
export default connect(store => (
{ discoverSearch: store.discoverSearch },
dispatch => dispatch
))(DiscoverSearchResultsContainer)
You have parenthesis wrong, this is equivalent to:
connect(store => {
// this does nothing
{ discoverSearch: store.discoverSearch };
return dispatch => dispatch;
})(...)
Which is actually the same as
connect(state => state)(...)
What you probably meant to write was:
connect(store = ({ discoverSearch: store.discoverSearch }),
dispatch => dispatch)(...)
EDIT: Remove unnecessary dispatch
As commented, mapping the dispatch is useless, you can just do
const mapStateToProps = state => ({ discoverSearch: state.discoverSearch });
connect(mapStateProps)(Component);
The easiest to way use connect is to separate out mapStateToProps and mapDispatchToProps into their own function so as to avoid making syntactical errors and if you are anyways returning dispatch as the second parameter you might as well not use it since if the second parameter to connect is empty, dispatch is returned by default.
According to the connect documentaion
[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or
Function): If an object is passed, each function inside it is assumed
to be a Redux action creator. An object with the same function names,
but with every action creator wrapped into a dispatch call so they may
be invoked directly, will be merged into the component’s props.
If a function is passed, it will be given dispatch as the first
parameter. It’s up to you to return an object that somehow uses
dispatch to bind action creators in your own way. (Tip: you may use
the bindActionCreators() helper from Redux.)
If your mapDispatchToProps function is declared as taking two
parameters, it will be called with dispatch as the first parameter and
the props passed to the connected component as the second parameter,
and will be re-invoked whenever the connected component receives new
props. (The second parameter is normally referred to as ownProps by
convention.)
If you do not supply your own mapDispatchToProps function or object
full of action creators, the default mapDispatchToProps
implementation just injects dispatch into your component’s props.
You could use your connect statmeent like
const mapStateToProps = store => {
return { discoverSearch: store.discoverSearch }
}
export default connect(mapStateToProps)(DiscoverSearchResultsContainer)
When you use connect like
export default connect(store => (
{ discoverSearch: store.discoverSearch },
dispatch => dispatch
))(DiscoverSearchResultsContainer)
You actually have your () at the wrong place as you want to return { discoverSearch: store.discoverSearch } and not { discoverSearch: store.discoverSearch }, dispatch => dispatch. It should be
export default connect(store => (
{ discoverSearch: store.discoverSearch })
)(DiscoverSearchResultsContainer)
The above snippet would be the same as
export default connect(store => {
return { discoverSearch: store.discoverSearch }
}
)(DiscoverSearchResultsContainer)
which is what you need. However you must go with the first approach
Is it possible that your DiscoverSearchReducer is returning a new object for each dispatch, even on a no-op?
function reduceSomething(state, action) {
if action.type == "forMe":
return {action.payload}
else
return {...state}
}
rather than:
return state
Because the former will give you a new object for every dispatch, with the expected content, but connect will be unable to suppress passing props to your component.
It's a weird one but it matches your symptoms.
Do I need to use HOC if I only need to change mapDispatchToProps and mapStateToProps but not change the presentational component?
For example:
export class SomeContainer extends Component {...}
const mapStateToProps = state => {
//IMPORT HERE STATE
};
const mapDispatchToProps = (dispatch, ownProps) => ({
...someFn,
//IMPORT HERE OTHER DISPATCH FN
});
And reuse it in other components by changing connectFn.
I just wrapped it up in different components
import React, {Component, PropTypes} from "react";
import SomeContainer from "./index";
export class ReuseSomeContainerA extends Component {
render() {
return (
<SomeContainer {...this.props}/>
);
}
}
const mapDispatchToProps = (dispatch, ownProps) => ({
//...customDispatchsA
});
export const enhance = connect(() => state => ({
//customPropsA
}), mapDispatchToProps);
export default enhance(ReuseSomeContainerA );
And likewise with another used SomeContainer
export class ReuseSomeContainerB extends Component {
render() {
return (
<SomeContainer {...this.props}/>
);
}
}
const mapDispatchToProps = (dispatch, ownProps) => ({
//...customDispatchsB
});
export const enhance = connect(() => state => ({
//customPropsB
}), mapDispatchToProps);
export default enhance(ReuseSomeContainerB);
In addition to these functions do not change anything.
Is there a way of more correct composition mapDispatchToProps and mapStateToProps only?
I don't see why you would need to wrap anything in these ReuseSomeContainer components.
As I understand the question, you have the component SomeContainer which expects some props (values and functions) from redux. You want those values and functions to be different in different parts of your app, but SomeContainer would do the same thing everywhere.
You would just export SomeContainer and the use it in different locations.
Component A:
const ConnectedContainerA = connect(mapStateToPropsA, mapDispatchToPropsA)(SomeContainer);
Component B:
const ConnectedContainerB = connect(mapStateToPropsB, mapDispatchToPropsB)(SomeContainer);
This is my Selector , I can able to get data with in the selector but don't know how to call this into view (Component) ,
import {todos} from '../reducers/todos';
import { createSelector } from 'reselect'
var visibilityFilter='SHOW_ALL';
var getVisibilityFilter = (state) => visibilityFilter;
var getTodos = (state) => todos;
export const getVisibleTodos = createSelector(
[ getVisibilityFilter, getTodos ],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
)
export default getVisibleTodos;
I have Tried in Component
<button onClick={()=>props.getVisibleTodos(props.SHOW_ALL , props.experimentData.lights)}> SHOW_COMPLETED</button>
Error
Uncaught Error: Actions must be plain objects. Use custom middleware
for async actions.
Blockquote
Help me Out ...
You should call the selector on the connect function like this:
import { connect } from 'react-redux';
import getVisibleTodos from 'your/selector/file';
function YourComponent({ visibleTodos }) {
// You can access visibleTodos inside your component
// because now it's on the props
return (
<div>
//...
</div>
);
}
const mapping = (state, props) => ({
visibleTodos: getVisibleTodos(state, props),
});
connect(mapping)(YourComponent);
Inside the mapping function, you have access to the state and props for the current component. Keep in mind that all selectors must receive the redux store in order to query the data.
Good luck!
I expect that your Redux store state looks something like this:
{
todos: [
{
id: 1,
text: 'Buy milk',
completed: false
},
...
],
visibilityFilter: 'SHOW_ALL'
}
If it is so, then you have to rewrite your getVisibilityFilter and getTodos selectors.
const getVisibilityFilter = (state) => state.visibilityFilter;
const getTodos = (state) => state.todos;
Previously you weren't accessing the values from your state, using this edited functions you are. See how I am using dot notation to access the keys of state, which is nothing more than a JavaScript Object.
Also, when u want to use a selector, you should use it in a container component, where u can access the store's state using mapStateToProps function.
The container could look something like this:
import React from 'react';
import { connect } from 'react-redux';
import { getVisibleTodos } from './selectors.js';
import TodosList from './TodosList.jsx';
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state)
}
}
const VisibleTodosList = connect(
mapStateToProps
)(TodosList);
export default VisibleTodosList;
Where the TodosList component is your own component that displays the todos. It will receive all visible todos using props (this.props.todos).
In my opinion, selectors aren't called from your view (presentational) components, they are meant to be used in containers, where you can access the application's data.
If you want to learn more about containers and presentational components, take a look at this article, it's worth reading.