Angular 2 with ngrx: using selectors breaks change detection? - javascript

I'm currently building an Angular2 app using ngrx to manage state. Run into an odd problem where if I populate an observable using store.select([state]) everything seems to work fine, but if I use store.let([getState]) I can't seem to subscribe to the Observable from that point on.
I have a component:
export class Component implements OnInit {
items: Observable<Item[]>;
currentCategory: Observable<Category> = null;
constructor(
private store: Store<State>
) {
this.currentCategory = store.select(s => s.categories.selected);
// this.currentCategory = store.let(storeRoot.getSelectedCategory);
this.items = store.select(s => s.items);
}
ngOnInit() {
this.currentCategory.subscribe(cat => {
this.Items = this.store.select(s => s.items.budgetItems)
.map(items => items.filter(item => item.category === cat.name ));
});
The idea is to filter the list of items by category name when the category name changes. This works fine when I get state directly with the store.select statements, but when I try to use selectors in the store, the Observable seems to be assigned once and any changes to it no longer trigger the subscribe block.
export interface AppState {
items: fromBudgetItem.State;
categories: fromCategory.State;
};
export const composeStore = compose(storeLogger(), combineReducers)({
items: fromBudgetItem.default,
categories: fromCategory.default
});
// Categories selectors
export function getCategoriesState(state: Observable<AppState>) {
return state.select(s => s.categories);
}
export const getSelectedCategory = compose(fromCategory.getSelectedCategory, getCategoriesState);
The 'compose' statement refers to a selector in the reducer:
export interface State {
selected: null
};
export function getSelectedCategory(state$: Observable<State>) {
return state$.select(state => state.selected);
}
Any advice anyone can give? This seems to be the way they do it in the ngrx example app.

Related

Any change to redux store my causes component to re-render

I'm doing some testing on my UI and I've noticed that if any state changes in my redux store my component (shown below) re-renders and restarts with embedded video at 0. If I type in a redux-connected text field, it remounts, if a status notification hits the store, it remounts, etc.
I have no idea how to fix this and I could really use some help figuring out how to go after the bug.
tldr; How can I stop my VideoPlayer from re-rendering each time something changes in my redux store?
redux-toolkit
react
component
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm);
// renders output here in the same component
return (
...
{sourceContentFound === false ? (
<VideoPlayerDisabled />
) : (
<VideoPlayerController
title={title}
description={description}
sourceAddress={iframeURL}
author={authorName}
timeStamp={{ startTime: 0 }}
/>
)}
)
...
}
formSlice
export const dynamicFormSlice = createSlice({
name: 'dynamicForm',
initialState,
reducers: {
updateField: (state, action) => {
state = action.payload;
return state;
}
},
});
export const selectDynamicForm = createSelector(
(state: RootState): dynamicFormState => state.dynamicForm,
dynamicForm => dynamicForm
);
statusHandlerSlice
I don't think this component does anything crazy, per-say, but I have a notification appear when the video conditions are met. When it goes back down clearStatus the video player restarts.
export const statusHandlerSlice = createSlice({
name: 'statusHandler',
initialState,
reducers: {
setStatus: (state, action: PayloadAction<IStatusObject>) => {
const { type, display, message } = action.payload;
state.status = {
...action.payload,
message: message.charAt(0).toUpperCase() + message.slice(1),
};
if (display === 'internal-only' || display === 'support-both') {
statusLogger(type, message);
}
},
clearStatus: state => {
state.status = {
type: 'success',
data: {},
message: '',
display: 'internal-only',
key: '',
};
},
},
});
export const { setStatus, clearStatus } = statusHandlerSlice.actions;
export const selectStatus = (state: RootState): IStatusObject =>
state.statusHandler.status;
Your MyComponent is re-render every time redux store state change is because you have a selector in it
You could stop this to happen by, add an equalityFn to useSelector.
You can write your own equalityFn or use some existing function from a library that supports deep comparison.
Ex: Use lodash isEqual
import { isEqual } from 'lodash';
const MyComponent = () => {
...
// data used in the VideoPlayer is descructured from this variable:
const formState = useSelector(selectDynamicForm, isEqual);
By default, useSelector use a shallow compare which can't detect deep changes inside your object, change to a deep comparison function like isEqual will help you to do that, but It's not recommended for all selector since there will be a performance impact.
Live Example:
I suggest either creating a custom equalFn to compare the data you're using in the current component or do not select the whole slice, maybe some properties change is unnecessary for your component. like:
const { data } = useSelector(store => store.sliceA, shallowEq);
// console.log(data) => { a: "foo", b: "bar" }
// data.b is useless but once it is changed, the component will re-render as well
return <Typography>{data.a}</Typography>
You should install React Devtools, turn on profiler, remember to check Record why each component rendered while profiling in setting to see what is causing re-rendering. sometimes custom hooks in libraries trigger re-rendering.
whyDidYouRender
is a good choice too

Ember component is not re-rendering on change of #tracked variable

My UserTag component is only re-rendering on page refresh. I expect it to refresh when action saveRoles(updatedUserRoles) is used, since it depends on an #tracked variable.
UserRoles is a component from an imported dependency in package.json.
UserTag component:
export default class UserTag extends Component {
constructor() {
super(...arguments);
this.tags = this.updateTags(this.args.tags);
}
#tracked tags;
updateTags(tags) {
if (tags) {
if (!Array.isArray(tags)) tags = Array.of(tags);
return tags.map((tag, index) => this.createTag(tag, index));
}
return tags;
}
...
Defined in my template:
<UserTag #tags={{this.sortedUserRoles}} />
In my controller:
#tracked persistedRoles = null;
#computed('persistedRoles')
get sortedUserRoles() {
console.log('sorting user roles...');
return this.persistedRoles.sort();
}
#action
saveRoles(updatedUserRoles) {
... // Some manipulating of updated roles for AJAX call
this.persistedRoles = [...updatedUserRoles];
}
I've tried adding a console.log in sortedUserRoles() to see if it is being called when I save permissions, but it is not being used. If I add {{this.sortedUserRoles}} outside of the #tags=..., it is called and the sorted roles are shown, which leads me to believe it ignored the tracking when set to a property for a component. Is this the case? (Note: #tags is #tracked in <UserTag>)
Versions:
"ember-cli": 3.22.0
"ember-data": "~3.18.0"
Your problem is that you copy this.args.tags to this.tags in the constructor. This code only runs once.
this.tags = this.updateTags(this.args.tags);
So later when you do
this.persistedRoles = ...
you only update this.args.tags but not this.tags. And because this.args.tags is not used in the template sortedUserRoles is not used either and so ember will have no reason to call the getter.
So the solution is to use this.args.tags in your component directly. If you need to transform the data use a getter:
get tags() {
let tags = this.args.tags;
if (tags) {
if (!Array.isArray(tags)) tags = Array.of(tags);
return tags.map((tag, index) => this.createTag(tag, index));
}
return this.args.tags;
}
Also remove the #computed('persistedRoles').

Creating a var referencing a store from redux

I am currently working on creating a var that references a store from redux. I created one but within the render(). I want to avoid that and have it called outside of the render. Here is an example of it. I was recommended on using componentWillMount(), but I am not sure how to use it. Here is a snippet of the code I implemented. Note: It works, but only when I render the data. I am using double JSON.parse since they are strings with \
render() {
var busData= store.getState().bus.bus;
var driverData= store.getState().driver.gdriveras;
var dataReady = false;
if (busData&& driverData) {
dataReady = true;
console.log("========Parsing bus data waterout========");
var bus_data_json = JSON.parse(JSON.parse(busData));
console.log(bus_data_json);
console.log("========Parsing driver data waterout========");
var driver_data_json = JSON.parse(JSON.parse(driverData));
console.log(driver_datat_json);
busDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
driverDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
...
}
}
Here is an example of react-redux usage that will probably help you.
Don't forget to add StoreProvider to your top three component (often named App).
I warned you about the fact that React and Redux are not meant to be used by beginner javascript developer. You should consider learn about immutability and functional programming.
// ----
const driverReducer = (state, action) => {
switch (action.type) {
// ...
case 'SET_BUS': // I assume the action type
return {
...state,
gdriveras: JSON.parse(action.gdriveras) // parse your data here or even better: when you get the response
}
// ...
}
}
// same for busReducer (or where you get the bus HTTP response)
// you can also format your time properties when you get the HTTP response
// In some other file (YourComponent.js)
class YourComponent extends Component {
render() {
const {
bus,
drivers
} = this.props
if (!bus || !drivers) {
return 'loading...'
}
const formatedBus = bus.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
const formatedDrivers = drivers.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
// return children
}
}
// this add bus & drivers as props to your component
const mapStateToProps = state => ({
bus: state.bus.bus,
drivers: state.driver.gdriveras
})
export default connect(mapStateToProps)(YourComponent)
// you have to add StoreProvider from react-redux, otherwise connect will not be aware of your store

Exposing the state of a react widget

I have made a react UI widget thats let's the user select a number of different times and dates. The user's current selection is stored in the state of a top level component, DateTimePicker. I then have a widget wrapper like so:
import ...
export default {
new: (args) => {
const store = {
reactElement: <DateTimePicker
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return store.reactElement.getState(); // DOESN'T WORK
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
I want to add a validation to make sure that at least X days/times are selected, but this validation needs to be implemented outside of the widget.
For this, I'll need someway of asking the widget of it 's state. i.e. what has the user selected? Although it seems like the state of the class is not part of the public api of a react component.
How can I acess the state, or is there another way I'm missing?
The solution to doing things imperatively from the parent to the child usually involves getting a ref to the child component. Something along these lines:
export default {
new: (args) => {
let myRef = React.createRef();
const store = {
reactElement: <DateTimePicker
ref={myRef}
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return myRef.current.getState();
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
With ref={myRef} added as a prop, whenever DateTimePicker gets mounted, it will assign a reference to the mounted component to myRef.current. You can then use that reference to interact directly with the most recently mounted component.

How to bind React state to RxJS observable stream?

can someone help me how to bind React State to RxJS Observable? I did sth like
componentDidMount() {
let source = Rx.Observable.of(this.state.val)
}
The ideal result is, whenever this.state.val updated (via this.setState(...)) source get updated too, so I can combine source with other RxJS observable stream.
However, in this case, source only get updated once, even after this.state.val is updated and component is re-rendered.
// Ideal result:
this.state.val = 1
source.subscribe(val => console.log(x)) //=> 1
this.state.val = 2
source.subscribe(val => console.log(val)) //=> 2
// Real result:
this.state.val = 1
source.subscribe(val => console.log(x)) //=> 1
this.state.val = 2
source.subscribe(val => console.log(val)) //=> 1 ???WTH
It might be because componentDidMount() only invoked once in React lifetime. so I move source to componentDidUpdate() which is invoked everytime after component is rendered. However, the result still remain the same.
So the question is how to make source updated whenever this.state.val updated?
Updated: Here is the solution I used to solve the prob, using Rx.Subject
// Component file
constructor() {
super(props)
this.source = new Rx.Subject()
_onChangeHandler(e) {
this.source.onNext(e.target.value)
}
componentDidMount() {
this.source.subscribe(x => console.log(x)) // x is updated
}
render() {
<input type='text' onChange={this._onChangeHandler} />
}
//
Update
To abstract out some of the below complexity, use recompose's mapPropsStream or componentFromStream. E.g.
const WithMouseMove = mapPropsStream((props$) => {
const { handler: mouseMove, stream: mouseMove$ } = createEventHandler();
const mousePosition$ = mouseMove$
.startWith({ x: 0, y: 0 })
.throttleTime(200)
.map(e => ({ x: e.clientX, y: e.clientY }));
return props$
.map(props => ({ ...props, mouseMove }))
.combineLatest(mousePosition$, (props, mousePosition) => ({ ...props, ...mousePosition }));
});
const DumbComponent = ({ x, y, mouseMove }) => (
<div
onMouseMove={mouseMove}
>
<span>{x}, {y}</span>
</div>
);
const DumbComponentWithMouseMove = WithMouseMove(DumbComponent);
Original Post
For a slightly updated answer to the OP's updated answer, using rxjs5, I came up with the following:
class SomeComponent extends React.Component {
constructor(props) {
super(props);
this.mouseMove$ = new Rx.Subject();
this.mouseMove$.next = this.mouseMove$.next.bind(this.mouseMove$);
this.mouseMove$
.throttleTime(1000)
.subscribe(idx => {
console.log('throttled mouse move');
});
}
componentWillUnmount() {
this.mouseMove$.unsubscribe();
}
render() {
return (
<div
onMouseMove={this.mouseMove$.next}
/>
);
}
}
Some notable additions:
onNext() is now next()
binding the observable next method allows it to be passed directly to the mouseMove handler
streams should be unsubscribed in componentWillUnmount hook
Furthermore, the subject streams initialized in the component constructor hook can be passed as properties to 1+ child component(s), which could all push to the stream using any of the observable next/error/complete methods. Here's a jsbin example I put together demonstrating multiple event streams shared between multiple components.
Curious if anyone has ideas on how to better encapsulate this logic to simplify stuff like binding and unsubscribing.
One option could be to use Rx.Observable.ofObjectChanges > cf. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/ofobjectchanges.md.
However :
It uses Object.observe which is not a standard feature, hence will have to be polyfilled in some browsers and is actually being removed from inclusion in ecmascript (cf. http://www.infoq.com/news/2015/11/object-observe-withdrawn). Not the choice for the future, but it is easy to use, so if it is just for your own needs, why not.
Other option is to use a subject in one of the three methods at your disposal according to your use case : shouldComponentUpdate, componentWillUpdate, componentDidUpdate. Cf. https://facebook.github.io/react/docs/component-specs.html for when each function is executed. In one of these methods, you would check if this.state.val has changed, and emits its new value on the subject if it did.
I am not a reactjs specialist, so I guess they might be other options.
Although a subject will work, I think the best practice is to avoid using a subject when you can use an observable. In this case you can use Observable.fromEvent:
class MouseOverComponent extends React.Component {
componentDidMount() {
this.mouseMove$ = Rx.Observable
.fromEvent(this.mouseDiv, "mousemove")
.throttleTime(1000)
.subscribe(() => console.log("throttled mouse move"));
}
componentWillUnmount() {
this.mouseMove$.unsubscribe();
}
render() {
return (
<div ref={(ref) => this.mouseDiv = ref}>
Move the mouse...
</div>
);
}
}
ReactDOM.render(<MouseOverComponent />, document.getElementById('app'));
Here it is on codepen....
It seems to me that there are other times when a Subject like the best choice, like when a custom React component executes a function when an event occurs.
I would highly recommend reading this blog post on streaming props to a React component using RxJS:
https://medium.com/#fahad19/using-rxjs-with-react-js-part-2-streaming-props-to-component-c7792bc1f40f
It uses FrintJS, and applies the observe higher-order component for returning the props as a stream:
import React from 'react';
import { Observable } from 'rxjs';
import { observe } from 'frint-react';
function MyComponent(props) {
return <p>Interval: {props.interval}</p>;
}
export default observe(function () {
// return an Observable emitting a props-compatible object here
return Observable.interval(1000)
.map(x => ({ interval: x }));
})(MyComponent);
You can do it using hooks.
Here is a code sample
import { Observable, Subscription } from 'rxjs';
import { useState, useEffect } from 'react';
export default function useObservable<T = number | undefined>(
observable: Observable<T | undefined>,
initialState?: T): T | undefined {
const [state, setState] = useState<T | undefined>(initialState);
useEffect(() => {
const subscription: Subscription = observable.subscribe(
(next: T | undefined) => {
setState(next);
},
error => console.log(error),
() => setState(undefined));
return () => subscription.unsubscribe();
}, [observable])
return state;
}

Categories

Resources