The Redux library easy-peasy has a function called computed which is an alternative to standard Redux selectors:
import { computed } from 'easy-peasy'
const model = {
session: {
totalPrice: computed(state => state.price + state.tax)
}
}
And then the selector is called in the component like this:
import { useStoreState } from 'easy-peasy'
function TotalPriceOfProducts() {
const totalPrice = useStoreState(state => state.products.totalPrice)
return <div>Total: {totalPrice}</div>
}
The problem is that there doesn't seem to be a way to pass inputs to the selector. If I need a specific object in an array in the state, I can't pass the ID of the object as an input. My only option is to do this on the component side. Redux selectors have the advantage of being functions, so I can pass inputs to be used in the selector logic.
Anyone use easy-peasy come across this problem before?
I can't pass the ID of the object as an input
To enable this, you have to return a function as your computed property.
Here is an example, exposing a computed function getTodoById:
import { computed } from 'easy-peasy'
const model = {
todos: [],
getTodoById: computed(state => {
// Note how we are returning a function instead of state
// 👇
return (id) => state.todos.find(t => t.id === id)
})
}
Then you can use it like this:
import { useStoreState } from 'easy-peasy'
function Todo({ id }) {
const getTodoById = useStoreState(state => state.getTodoById)
const matchingTodo = getTodoById(id);
// ... etc
}
Related
I'm struggling to figure out how to make properties of an array reactive. Is this possible? In the example below, the filteredResults array itself is reactive, and working, but neither the resultCountRef (wrapped in reactive()) nor the resultCount fields are reactive. In otherwords, if I click the Filter for Apples button, the filteredResults changes to just the one item, but the two count fields remain at 3. Note that using {{filteredResults.length}} in the template does work as expected. Here is a working sample.
And here is the code (a Search.vue composition API component, and a useFilter composition function):
Search.vue:
<template>
<div>resultCountRef: {{resultCountRef}}</div>
<div>resultCount: {{resultCount}}</div>
<div>filteredResults.length: {{filteredResults.length}}</div>
<div>filteredResults: {{filteredResults}}</div>
<div>filters: {{filters}}</div>
<div><button #click="search()">Search</button></div>
<div><button #click="applyFilter('apples')">Filter for Apples</button></div>
</template>
<script>
import { reactive } from 'vue';
import useFilters from './useFilters.js';
export default {
setup(props) {
const products = reactive(['apples', 'oranges', 'grapes']);
const { filters, applyFilter, filteredResults } = useFilters(products);
const resultCountRef = reactive(filteredResults.value.length);
const resultCount = filteredResults.value.length;
return {
resultCountRef,
resultCount,
filteredResults,
filters,
applyFilter,
};
},
};
</script>
useFilters.js:
import { ref, onMounted } from 'vue';
function filterResults(products, filters) {
return products.filter((product) => filters.every(
(filter) => {
return product.includes(filter);
},
));
}
export default function useFilters(products) {
const filters = ref([]);
const filteredResults = ref([...products]);
const applyFilter = (filter) => {
filters.value.push(filter);
filteredResults.value.splice(0);
filteredResults.value.splice(0, 0, ...filterResults(products, filters.value));
};
return { filters, applyFilter, filteredResults };
}
UPDATED
const resultCountRef = reactive(filteredResults.value.length);
The reason why reactive is not working is because filteredResults.value.length returned a simple number and not referencing to anything.
From #JimCopper 's comment:
The returned object is simply providing an observable object that wraps the object/value/array passed in (in my case the filteredResults.length value). That returned object resultCountRef is the object that is tracked and can be then modified, but that's useless in my case and it's why reactive didn't work. )
As the resultCount depends on filteredResults, you can use computed to monitor the change
Here is the modified playground
setup(props) {
const products = reactive(['apples', 'oranges', 'grapes']);
const { filters, applyFilter, filteredResults } = useFilters(products);
// const resultCountRef = reactive(filteredResults.length);
const resultCount = computed(() => filteredResults.length) // use computed to react to filteredResults changes
return {
// resultCountRef,
resultCount,
filteredResults,
filters,
applyFilter,
};
},
The new doc does not have a very nice explanation on computed so i just quote it from the old doc explanation
A computed property is used to declaratively describe a value that depends on other values. When you data-bind to a computed property inside the template, Vue knows when to update the DOM when any of the values depended upon by the computed property has changed.
I have a redux store with multiple teams.
const store = {
selectedTeamId: 'team1';
teams: {
team1: { ... },
team2: { ... },
team3: { ... },
},
};
At any given time a teamId is set.
Now given that I must select the team using the ID each time I call mapStateToProps(), I feel this is cumbersome.
Instead of doing this all the time:
mapStateToProps({ selectedTeamId, teams }) {
return {
team: teams[selectedTeamId],
}
}
Can I pre-process the store using some middleware instead of repeating this pattern in map state to props?
Approach suggested by Redux docs is to create a selector for currently active team and reuse it across all components
// selector itself is a pure function of state
// usually put in separate file, or in file with reducer
const activeTeamSelector = state => state.teams.teams[state.teams.selectedTeamId]
// in connect
const mapStateToProps = (state) => ({
activeTeam: activeTeamSelector(state),
})
That, of course, if you are using combineReducers and teams reducer is called teams in state. If you aren't, and selectedTeamId and teams are contained right in your store, following will work
const activeTeamSelector = state => state.teams[state.selectedTeamId]
Notice how I only had to change selector for this, and not every mapStateToProps in all the components
read more about Normalizing Store State and Computing Derived Data in Redux docs
Using a middleware for this scenario isn't performant (if I understood your question correctly :) ). I will outline 3 options you can use to achieve this:
Option 1
return both selectedTeamId and teams in mapStateToProps, this will allow you to find the team you need for each selected id:
mapStateToProps({ selectedTeamId, teams }) {
return {
selectedTeamId,
teams
}
}
That way you can access these props in render:
render() {
const { teams, selectedTeamId } = this.props;
return <Team team={teams.find(team => team.id === selectedTeamId)} />
}
Note: <Team /> is just a component I made for demonstration
Option 2
you can use reselect library to avoid recomputing this prop:
import { createSelector } from 'reselect'
const teams = state => state.teams;
const selectedTeamId = state => state.selectedTeamId;
const subtotalSelector = createSelector(
teams,
selectedTeamId,
(teams, selectedTeamId) => items.find(team => team.id === selectedTeamId)
)
Option 3
Create an action that will dispatch 'SELECT_TEAM' with the teamId
export function setSelectedTeam(id) {
return { type: types.SELECT_TEAM, payload: id };
}
Create a reducer for that type and return selectedTeam:
[types.SELECT_TEAM]: (state, payload)=> {
return {
...state,
selectedTeam: state.teams.find(team => team.id === payload.id)
};
},
That way you can have a selector for selectedTeam
export const getSelectedTeam = state => state.selectedTeam;
Hope it helps
I eventually used reselect, with thanks to the recommendation of #jank.
One of things I wanted to do was abstract away the need for selectors to appear in mapStateToProps. In order to do that, I wrapped redux connect. This allows insertion of a denormalizer function before mapStateToProps.
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
const getActiveTeamId = state => state.activeTeamId;
const getAllTeams = state => state.teams;
const teamSelector = createSelector(
getActiveTeamId,
getAllTeams,
(activeTeamId, teams) => teams[activeTeamId],
);
function denormalizer(mapStateToProps) {
return state => {
return mapStateToProps({ team: teamSelector(state) });
};
}
export default function reConnect(mapStateToProps = null, actions = null) {
const denormalizedMapStateToProps = denormalizer(mapStateToProps);
return function callConnect(Component) {
return connect(denormalizedMapStateToProps, actions)(Component);
};
}
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)
}
}
// ...
}
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)
I'm now working with react-komposer and the container/component pattern, but it's left me wondering how to handle data input.
For example, an AddVehicleForm component has a container that pre-populates some fields with data from the database. With the standard React Komposer examples, this makes sense:
import { composeWithTracker } from 'react-komposer';
import { Vehicles } from '../../collections/vehicles.js';
import AddVehicleForm from '../components/AddVehicleForm.jsx';
const composer = ( props, onData ) => {
const subscription = Meteor.subscribe( 'vehicles' );
if ( subscription.ready() ) {
const curVehicles = Vehicles.find().fetch();
onData( null, { curVehicles } );
}
};
const Container = composeWithTracker( composer )( AddVehicleForm );
But, to keep the component truly unreliant on it's data source, you would also need to pass it a handleSubmit() function to submit to the database, would you not? Where would you put this function?
Alternatively, I can see how it wouldn't be hard to solve using TrackerReact. But, as React Komposer is so widely adopted, what's the common way to handle this case?
EDIT:
Just throwing out an idea, but is there any reason not to create a container component with submit handling methods and then wrap that with the composer function? Something akin to this:
import {composeWithTracker} from 'react-komposer';
import ClassroomDashboard from '/imports/components/classroomDashboard/ClassroomDashboard.jsx';
class ClassroomDashboardContainer extends React.Component {
onSubmitHandle(e) {
// check form data and submit to DB
}
render() {
return(
<ClassroomDashboard {...this.props} onSubmit={this.onSubmitHandle.bind(this)} />
)
}
}
function composerFunction(props, onData) {
const handle = Meteor.subscribe('classroom');
if (handle.ready()) {
const classroom = Classrooms.findOne(props.params.id);
onData(null, {classroom});
};
};
export default composeWithTracker(composerFunction)(ClassroomDashboardContainer);