In one of the features of my application, I'm syncing with Firebase to get a list of active events. The primary result of this means that the observable will be set repeatedly in real time. This is great, but it has led to some MobX troubles. To demonstrate, take a look at the relevant store:
import { observable, computed, autorun, extendObservable, reaction } from "mobx";
import { subscribe } from "./firebase";
class HomeStore {
#observable activeEvents = {};
#observable currentEvents = [];
constructor() {
console.log(this);
// subscribe( root => root.ref("meta/activeEvents"), this.activeEvents, "object" );
setTimeout(() => this.currentEvents["RE-VRC-16-1274"] = true, 100);
reaction(
() => Object.keys(this.currentEvents),
events =>
events.map( event =>
this.currentEvents[event] === true ?
subscribe( root => root.ref(`events/${event}`), this.currentEvents[event], "object") :
null
)
);
}
}
export default new HomeStore();
Here, the subscribe function will bind a specific part of my database to the specified observable (the setTimeout does essentially the same thing). What I am trying to achieve is such:
The state is empty, as specified by the original store state
My subscribe() function changes the values of children of this.currentEvents, which triggers a reaction to create new observables, and subscribe them to their appropriate path in the database
As these observables are changed by subscribe(), the render() function of my React Component changes. See below for that source
import { h, Component } from 'preact';
import { List, ListItem, Icon } from 'preact-mdl';
import { observer } from "mobx-observer";
import { icon, center } from "../style";
import HomeStore from "../stores/home";
#observer
export default class EventList extends Component {
constructor() {
super();
this.store = HomeStore;
}
render() {
console.log("EventList Renders", Object.assign({}, this.store.events));
return (
<List>
{Object.keys(this.store.currentEvents).map( event =>
<ListItem two-line>
<span class="mdl-list__item-primary-content">
<Icon icon="event" class="material-icons mdl-list__item-avatar" style={icon.avatar}></Icon>
<span>{ this.store.currentEvents[event].name ? this.store.currentEvents[event].name : "Loading..." }</span>
<span class="mdl-list__item-sub-title">{ event }</span>
</span>
</ListItem>
)
}
</List>
);
}
}
This seems like it should be pretty easy to do, but I am struggling. Am I doing this wrong? Is there a better approach? Any advice is very welcome!
Didn't read the rest of the question, but this won't fly: reaction(() => Object.keys(this.currentEvents). Mobx cannot track the addition of keys to an object, so for dynamically keyed data structures use the built-in maps from mobx, e.g.: activeEvents = observable.map()
Related
In React all props are updated and propagated to children automatically which is nice but it slows down and requires lots of optimization at some point.
So I'm building an app with SolidJS using Context + createStore patterng and I'm having problems with consuming that state.
I'd like to create AppProvider component that manages State props and Dispatch functions. The Provider will be performing all operations on appStore, implement functions and serve them all via AppContextState and AppContextDispatch providers.
Then I need to consume that data to update components that are dependent on it reactively.
Look at the code below:
/// index.tsx
import { render } from 'solid-js/web';
import { AppProvider } from '#/providers/AppProvider';
import App from './App';
render(() => (
<AppProvider>
<App />
</AppProvider>
), document.getElementById('root') as HTMLElement);
/// AppProvider.tsx
import { createContext, useContext, JSX } from 'solid-js';
import { createStore } from 'solid-js/store';
// Interfaces
interface IAppState {
isConnected: boolean;
user: { name: string; }
}
interface IAppDispatch {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
}
// Initialize
const initialState = {
isConnected: false,
user: { name: '' }
}
const initialDispatch = {
connect: () => {},
disconnect: () => {}
}
// Contexts
const AppContextState = createContext<IAppState>();
const AppContextDispatch = createContext<IAppDispatch>();
export const useAppState = () => useContext(AppContextState);
export const useAppDispatch = () => useContext(AppContextDispatch);
// Provider
export const AppProvider = (props: { children: JSX.Element }) => {
const [appStore, setAppStore] = createStore<IAppState>(initialState);
async function connect() {
setAppStore("isConnected", true);
setAppStore("user", "name", 'Chad');
}
async function disconnect() {
setAppStore("isConnected", false);
setAppStore("user", "name", '');
}
return (
<AppContextState.Provider value={appStore}>
<AppContextDispatch.Provider value={{ connect, disconnect }}>
{props.children}
</AppContextDispatch.Provider>
</AppContextState.Provider>
)
}
/// App.tsx
import { useAppState, useAppDispatch } from '#/providers/AppProvider';
export default function App() {
const { user, isConnected } = useAppState();
const { connect, disconnect } = useAppDispatch();
return (
<Show when={isConnected} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
)
}
This component will show a button that should run the connect function and update isConnected state and make the component within <Show> block visible but it doesn't do anything.
I verified that state is being updated by logging data of appStore in connect method.
When I change the component to depend on user.name instead isConnected it works
<Show when={user.name} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
However my app has many components depending on various data types, including boolean that for some doesn't work in this example with SolidJS.
I'd like to know what am I doing wrong here and understand what is the best way to share state between components. I keep reading documentation and fiddling with it but this particular problem bothers me for a past few days.
Plain Values in Solid cannot be tracked
The problem here is that primitive values / variables cannot be reactive in solid. We have two ways of tracking value access: Through function calls, and through property getters/proxies (which use signals under the hood).
So, what happens when you access a store property?
const state = useAppState();
createEffect(() => {
console.log(state.isConnected)
})
In this case, the property access is occurring within the effect, so it gets tracked, and reruns when the property value updates. On the other hand, with this:
const { isConnected } = useAppState();
We are accessing the property at the top level of the component (which is untracked and not reactive in solid). So even though we use this value in a context that is reactive (like the when prop in `), we can't run any special under-the-hood tracking to set up updates.
So why did user.name work?
The reason is that stores are deeply reactive (for primitives, objects and arrays), so
const { user } = useAppState();
Means that you are eagerly accessing the user object (so if the user property changes, you won't get updated), but the properties of the user object were not accessed yet, they only get accessed further on, in <Show when={user.name}>, so the property access user.name is able to be tracked.
Say I have two redux connected components. The first is a simple todo loading/display container, with the following functions passed to connect(); mapStateToProps reads the todos from the redux state, and mapDispatchToProps is used to request the state to be provided the latest list of todos from the server:
TodoWidgetContainer.js
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
The second redux component is intended to be applied to any component on a page so that component can indicate whether a global "loading" icon is displayed. Since this can be used anywhere, I created a helper function that wraps MapDispatchToProps in a closure and generates an ID for each component, which is used to make sure all components that requested the loader indicate that they don't need it anymore, and the global loader can be hidden.
The functions are basically as follows, with mapStateToProps exposing the loader visibility to the components, and mapDispatchToProps allowing them to request the loader to show or hide.
Loadify.js
function mapStateToProps(state) {
return {
openLoader: loaderSelectors.getLoaderState(state)
};
}
function mapDispatchToProps() {
const uniqId = v4();
return function(dispatch) {
return {
showLoader: () => {
dispatch(loaderActions.showLoader(uniqId));
},
hideLoader: () => {
dispatch(loaderActions.hideLoader(uniqId));
}
};
};
}
export default function Loadify(component) {
return connect(mapStateToProps, mapDispatchToProps())(component);
}
So now, if I have a component that I want to give access to the loader, I can just do something like this:
import Loadify from '...'
class DisplayComponent = new React.Component { ... }
export default Loadify(DisplayComponent);
And it should give it a unique ID, allow it to request the loader to show/hide, and as long as there is one component that is requesting it to show, the loader icon will show. So far, this all appears to be working fine.
My question is, if I would like to apply this to the todos component, so that that component can request/receive its todos while also being allowed to request the loader to show while it is processing, could I just do something like:
TodoWidgetContainer.js
import Loadify from '...'
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
const TodoContainer = connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
export default Loadify(TodoContainer);
And will redux automatically merge the objects together to make them compatible, assuming there are no duplicate keys? Or will it take only the most recent set of mapStateToProps/mapDispatchTo unless I do some sort of manual merging? Or is there a better way to get this kind of re-usability that I'm not seeing? I'd really rather avoid having to create a custom set of containers for every component we need.
connect will automatically merge together the combination of "props passed to the wrapper component", "props from this component's mapState", and "props from this component's mapDispatch". The default implementation of that logic is simply:
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
So, if you stack multiple levels of connect around each other , the wrapped component will receive all of those props as long as they don't have the same name. If any of those props do have the same name, then only one of them would show up, based on this logic.
Alright, here is what I would do. Create a higher order component (HOC) that adds a new spinner reference to your reducer. The HOC will initialize and destroy references to the spinner in redux by tying into the life cycle methods. The HOC will provide two properties to the base component. The first is isLoading which is a function that takes a boolean parameter; true is on, false is off. The second property is spinnerState that is a readonly boolean of the current state of the spinner.
I created this example without the action creators or reducers, let me know if you need an example of them.
loadify.jsx
/*---------- Vendor Imports ----------*/
import React from 'react';
import { connect } from 'react-redux';
import v4 from 'uuid/v4';
/*---------- Action Creators ----------*/
import {
initNewSpinner,
unloadSpinner,
toggleSpinnerState,
} from '#/wherever/your/actions/are'
const loadify = (Component) => {
class Loadify extends React.Component {
constructor(props) {
super(props);
this.uniqueId = v4();
props.initNewSpinner(this.uniqueId);;
this.isLoading = this.isLoading.bind(this);
}
componentWillMount() {
this.props.unloadSpinner(this.uniqueId);
}
// true is loading, false is not loading
isLoading(isOnBoolean) {
this.props.toggleSpinner(this.uniqueId, isOnBoolean);
}
render() {
// spinners is an object with the uuid as it's key
// the value to the key is weather or not the spinner is on.
const { spinners } = this.props;
const spinnerState = spinners[this.uniqueId];
return (
<Component isLoading={this.isLoading} spinnerState={spinnerState} />
);
}
}
const mapStateTopProps = state => ({
spinners: state.ui.spinners,
});
const mapDispatchToProps = dispatch => ({
initNewSpinner: uuid => dispatch(initNewSpinner(uuid)),
unloadSpinner: uuid => dispatch(unloadSpinner(uuid)),
toggleSpinner: (uuid, isOn) => dispatch(toggleSpinnerState(uuid, isOn))
})
return connect(mapStateTopProps, mapDispatchToProps)(Loadify);
};
export default loadify;
Use Case Example
import loadify from '#/location/loadify';
import Spinner from '#/location/SpinnerComponent';
class Todo extends Component {
componentWillMount() {
this.props.isLoading(true);
asyncCall.then(response => {
// process response
this.props.isLoading(false);
})
}
render() {
const { spinnerState } = this.props;
return (
<div>
<h1>Spinner Testing Component</h1>
{ spinnerState && <Spinner /> }
</div>
);
}
}
// Use whatever state you need
const mapStateToProps = state => ({
whatever: state.whatever.youneed,
});
// use whatever dispatch you need
const mapDispatchToProps = dispatch => ({
doAthing: () => dispatch(doAthing()),
});
// Export enhanced Todo Component
export default loadify(connect(mapStateToProps, mapDispatchToProps)(Todo));
I'm fairly new to react and struggle to update a custom component using componentDidMount and setState, which seems to be the recommended way of doing it. Below an example (includes an axios API call to get the data):
import React from 'react';
import {MyComponent} from 'my_component';
import axios from 'axios';
export default class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
GetData() {
return axios.get('http://localhost:5000/<route>');
}
componentDidMount() {
this.GetData().then(
(resp) => {
this.setState(
{data: resp.data}
)
}
)
}
render() {
return (
<MyComponent data={this.state.data} />
);
}
}
Doing console.log(this.state.data) just below render() shows that this.state.data does indeed get updated (from [] to whatever the API returns). However, the problem appears to be that MyComponent isn't rendered afresh by componentDidMount. From the Facebook react docs:
Setting state in this method will trigger a re-rendering.
This does not seem to be the case here: The constructor of MyComponent only gets called once (where this.props.data = []) and the component does not get rendered again. I'd be great if someone could explain why this is and whether there's a solution or a different way altogether to get the updating done.
UPDATE
I've added the code for MyComponent (minus some irrelevant features, as indicated by ...). console.log(data_array) prints an empty array.
import React from 'react';
class DataWrapper {
constructor(data) {
this._data = data;
}
getSize() {
return this._data.length;
}
...
}
export class MyComponent extends React.Component {
constructor(props) {
super(props);
this._dataWrapper = new DataWrapper(this.props.data);
this.state = {
data_array: this._dataWrapper,
};
}
render() {
var {data_array} = this.state;
console.log(data_array);
return (
...
);
}
}
You are falling victim to this antipattern.
In MyComponent constructor, which only gets called the first time it mounts, passed your empty array through new DataWrapper and now you have some local state which will never be updated no matter what your parent does.
It's always better to have one source of truth, just one state object anywhere (especially for things like ajax responses), and pass those around via props. In fact this way, you can even write MyComponent as a simple function, instead of a class.
class Example extends Component {
state = { data: [] }
GetData() { .. }
componentDidMount() {
this.GetData().then(res =>
this.setState({data: new DataWrapper(res.data)})
)
}
render() { return <MyComponent data={this.state.data} /> }
}
...
function MyComponent (props) {
// props.data will update when your parent calls setState
// you can also call DataWrapper here if you need MyComponent specific wrapper
return (
<div>..</div>
)
}
In other words what azium is saying, is that you need to turn your receiving component into a controlled one. Meaning, it shouldn't have state at all. Use the props directly.
Yes, even turn it into a functional component. This helps you maintain in your mind that functional components generally don't have state (it's possible to put state in them but ... seperation of concerns).
If you need to edit state from that controlled component, provide the functions through props and define the functions in the "master" component. So the master component simply lends control to the children. They want anything they talk to the parent.
I'm not posting code here since the ammendment you need to make is negligible. Where you have this.state in the controlled component, change to this.props.
Recently I started using mobx with react and mobx-react library.
I want to use functional React components to create my views.
I'd like to create a helper function, that takes selector function and Component, calls inject (with selector function as parameter) and observe on that Component - effectively connecting this component to mobx-react store (taken from Provider context) and providing only needed properties for this Component.
But I can't get it to work. Action is being dispatched, but views doesn't react to this change (store attributes does change, but Component doesn't react to this change).
Here's my helper function:
import { observer, inject } from 'mobx-react';
export function connect(selectorFunction, component) {
return inject(selectorFunction)(observer(component));
}
here's my Component:
import React from 'react';
import { connect } from 'utils';
const selector = (stores) => {
return {
counter: stores.counterStore.counter,
double: stores.counterStore.double,
increment: stores.counterStore.increment
};
};
const Counter = ({ counter, double, increment }) => {
return (
<div className="counter">
<p>{ counter }</p>
<p className="double">{ double }</p>
<button onClick={increment}>+1</button>
</div>
);
};
export default connect(selector, Counter);
and here's my store:
import { observable, computed, action } from 'mobx';
export default class Counter {
#observable counter = 0;
#action
increment = () => {
this.counter++;
}
#computed
get double() {
return this.counter * 2;
}
}
(Not showing Provider and other simple stuff, but it is set up properly).
Thanks! Every answer is much appreciated.
Looking at Mobx's documentation, it looks like your selector is doing things a bit wrong. It should return an object with stores, and not an object with values from stores. Try returning the actual counterStore instead:
const selector = (stores) => {
return {
counterStore: stores.counterStore
};
};
And use it like this in your component:
const Counter = ({ counterStore: { counter, double, increment } }) => {
return (
<div className="counter">
<p>{ counter }</p>
<p className="double">{ double }</p>
<button onClick={increment}>+1</button>
</div>
);
};
I am making a Higher-Order Component in my React.js (+ Redux) app, to abstract the functionality to filter a list of elements with the string received from an input element.
My filtering HOC is,
filter.js
import React, { Component } from 'react'
export default function Filter(FilteredComponent) {
return class FilterComponent extends Component {
constructor(props) {
super(props)
}
generateList() {
if (this.props.searchTerm !== undefined) {
let re = new RegExp(state.searchTerm,'gi')
return this.props.currencyList.filter((c) => c.match(re))
}
else {
return this.props.currencyList
}
}
render() {
return (
<FilteredComponent
filteredList={this.generateList()}
{...this.props}
/>
)
}
}
}
Right now, I am unable to access the filteredList as props.filteredList in the SearchResults component.
The component to display the list is
SearchResults.js
import React from 'react'
const SearchResults = (props) => {
const listData = props.filteredList.map (item => <div>{item}</div>)
return (
<div>
Here are the search results.
<br />
<input
type="text"
value={props.searchTerm}
onChange={props.setSearchTerm}
/>
{listData}
</div> ) }
export default SearchResults
How do I go on about this?
EDIT:
Adding the container component for greater clarity:
SearchContainer.js
import {connect} from 'react-redux'
import SearchResults from '../components/SearchResults'
import * as a from '../actions'
import Filter from '../enhancers/filter'
const getSearchTerm = (state) => (state.searchTerm === undefined) ? '' : state.searchTerm
const mapStateToProps = (state) => {
return {
searchTerm: getSearchTerm(state),
currencyList: state.currencyList
}
}
const mapDispatchToProps = (dispatch) => {
return {
setSearchTerm: (e) => {
dispatch(a.setSearchTerm(e.target.value))
}
}
}
const SearchResultsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(SearchResults)
export default Filter(SearchResultsContainer)
Let’s first think of components as a function that takes a props and returns a Virtual DOM.
Thus the SearchResult component takes these props:
filteredList
searchTerm
setSearchTerm
The higher-order-component created created by connect() provides these props:
searchTerm
currencyList
The Filter() higher-order component:
takes currencyList
provides filteredList
Therefore, you have to wire it like this so that each part receives the props it needs:
connect(...) → Filter → SearchResult
It should look like this:
export default connect(...)(Filter(SearchResult))
Or if you use recompose:
const enhance = compose(connect(...), Filter)
export default enhance(SearchResult)
compose() wraps the components from right to left. Therefore, the leftmost higher-order component becomes the outermost one. This means the props will flow from left to right.
Please note that state.searchTerm in FilterComponent#generateList should be this.props.searchTerm.
What is 'state.searchTerm' in your wrapper function? I have a feeling you mean this.props.searchTerm. Also, you don't need an empty constructor in es6 classes. Also, this is work better done by a selector in your mapstatetoprops on the container.
Edit:
Also, you need to wrap the actual 'dumb' component, not the result of your connect call. That way your redux store is connected to your Filter component and will be rerendered when you're store changes.
generateList() is not reactive. It does not get triggered when the search term is changed.
SearchResults should be stateful and the container component. The list component should respond to change in the search term by receiving the search term as props. generateList should be the functionality of componentWillReceiveProps of the list component.