Mobx observer component not re-rendering on state change - javascript

I am using Mobx in my react app for global state. It has been working fantastic and exactly as expected until now. I am storing the mouse position in an SVG element in the store and hoping to utilize this state in a number of components. I can tell the state is changing correctly by logging inside the store, but for some reason my CrossHairs component is not re-rendering despite being wrapped in an observer HOC and featuring dereferenced store values. Any help would be much appreciated.
Store:
import PropTypes from 'prop-types';
import React, { createContext, useContext } from 'react';
import { webSocket } from 'rxjs/webSocket';
import { makeAutoObservable } from 'mobx';
import { PriceDataStore } from './_priceDataStore';
import { groupByFunction } from './utils/rxjs/groupByFunction';
import { TradePlanStore } from './_tradePlanStore';
export class WebsocketStore {
websocketSubject = webSocket('ws://localhost:8080/socket');
chartSubject;
priceDataStore;
tradePlanStore;
mouseCoordinates;
constructor() {
makeAutoObservable(this);
this.priceDataStore = new PriceDataStore(this);
this.tradePlanStore = new TradePlanStore(this);
this.chartSubject = groupByFunction(
'messageType',
'getChart',
this.websocketSubject
);
this.mouseCoordinates = {
x: 0,
y: 0,
};
}
sendWebsocketMessage = (msg) => {
this.websocketSubject.next(msg);
};
updateCoordinates = (coordinates) => {
this.mouseCoordinates = { x: coordinates[0], y: coordinates[1] };
};
}
const WebsocketContext = createContext(null);
export const useWebsocketStore = () => useContext(WebsocketContext);
export const WebSocketProvider = ({ store, children }) => {
return (
<WebsocketContext.Provider value={store}>
{children}
</WebsocketContext.Provider>
);
};
WebSocketProvider.propTypes = {
children: PropTypes.any,
store: PropTypes.any,
};
Place where state is being changed, this seems to be working:
const websocketStore = useWebsocketStore();
useEffect(() => {
select(zoomRef.current)
.on('mouseenter', () => {
setShowCrossHairs(true);
})
.on('mousemove', (event) => {
const coordinates = pointer(event);
websocketStore.updateCoordinates(coordinates);
})
.on('mouseleave', () => {
setShowCrossHairs(false);
});
}, [websocketStore.updateCoordinates]);
Component that is not triggered to re-render on mouseCoordinates state change in store despite being wrapped in observer and dereferencing mouseCoordinates values.
import PropTypes from 'prop-types';
import React, { useEffect } from 'react';
import { select, pointer } from 'd3';
import { colors } from '../../../../../../colorVars';
import { useWebsocketStore } from '../../../../../../stores/websocketStore/websocketStore';
import { observer } from 'mobx-react-lite';
const ChartCrossHairs = observer(({
size,
bottomPadding,
showCrossHairs,
}) => {
const websocketStore = useWebsocketStore();
const x = websocketStore.mouseCoordinates.x;
const y = websocketStore.mouseCoordinates.y;
return (
<g>
{showCrossHairs && (
<>
<line
strokeDasharray="1, 1"
x1={0}
x2={size}
y1={y}
y2={y}
stroke={colors.white}
/>
<line
strokeDasharray="1, 1"
y1={0}
y2={size + bottomPadding}
x1={x}
x2={x}
stroke={colors.white}
/>
</>
)}
</g>
);
});

Related

React-Leaflet LocateControl component - keeps duplicating on each refresh

I'm using Locate Control in React Leaflet, but the Locate Control buttons are always duplicated, and sometimes I get 3 or 4 of them (see image below). I'm running the function through a useEffect with empty dependency to only fire it once, but no matter. I can target the class with display: none, but then both disappear. I feel like this might be an issue with Locate Control library? Really not sure. Open to any help or ideas.
import { useEffect } from "react"
import { useMap } from "react-leaflet"
import Locate from "leaflet.locatecontrol"
import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"
const AddLocate = () => {
const map = useMap()
useEffect(() => {
const locateOptions = {
position: "bottomleft",
flyTo: true,
}
const locateControl = new Locate(locateOptions)
locateControl.addTo(map)
}, [])
return null
}
export default AddLocate;
Looks like you use a package made for leaflet. Which should for the most parts be okay. However the way you add the control is not really the react-leaflet way, where we want to add add components rather than add "stuff" directly to the map.
Below you can see how easy it is to implement a location component that you simply just can add as component within your MapContainer.
import { ActionIcon } from "#mantine/core";
import React, { useState } from "react";
import { useMapEvents } from "react-leaflet";
import { CurrentLocation } from "tabler-icons-react";
import LeafletControl from "./LeafletControl";
interface LeafletMyPositionProps {
zoom?: number;
}
const LeafletMyPosition: React.FC<LeafletMyPositionProps> = ({ zoom = 17 }) => {
const [loading, setLoading] = useState<boolean>(false);
const map = useMapEvents({
locationfound(e) {
map.flyTo(e.latlng, zoom);
setLoading(false);
},
});
return (
<LeafletControl position={"bottomright"}>
<ActionIcon
onClick={() => {
setLoading(true);
map.locate();
}}
loading={loading}
variant={"transparent"}
>
<CurrentLocation />
</ActionIcon>
</LeafletControl>
);
};
export default LeafletMyPosition;
And for LeafletControl I just have this reusable component:
import L from "leaflet";
import React, { useEffect, useRef } from "react";
const ControlClasses = {
bottomleft: "leaflet-bottom leaflet-left",
bottomright: "leaflet-bottom leaflet-right",
topleft: "leaflet-top leaflet-left",
topright: "leaflet-top leaflet-right",
};
type ControlPosition = keyof typeof ControlClasses;
export interface LeafLetControlProps {
position?: ControlPosition;
children?: React.ReactNode;
}
const LeafletControl: React.FC<LeafLetControlProps> = ({
position,
children,
}) => {
const divRef = useRef(null);
useEffect(() => {
if (divRef.current) {
L.DomEvent.disableClickPropagation(divRef.current);
L.DomEvent.disableScrollPropagation(divRef.current);
}
});
return (
<div ref={divRef} className={position && ControlClasses[position]}>
<div className={"leaflet-control"}>{children}</div>
</div>
);
};
export default LeafletControl;
I would do some debugging to that useEffect to see if it's only happening once. It's possible the entire component is mounted multiple times.

React Mobx can't display observable contents, very simple app

Very simple app, I'm trying to display content from my API using Mobx and Axios, here's my Axios agent.ts:
import { ITutorialUnit } from './../model/unit';
import axios, { AxiosResponse } from "axios";
//set the base URL
axios.defaults.baseURL = "http://localhost:5000/api";
//store our request in a const
const responseBody = (response: AxiosResponse) => response.data;
const requests = {
get: (url: string) => axios.get(url).then(responseBody),
};
//create a const for our activty's feature,all our activities' request are go inside our Activities object
const TutorialUnits = {
list: ():Promise<ITutorialUnit[]> => requests.get("/tutorialunits"),
};
export default{
TutorialUnits
}
then I call this agent.s in a store:
import { ITutorialUnit } from "./../model/unit";
import { action, observable } from "mobx";
import { createContext } from "react";
import agent from "../api/agent";
class UnitStore {
#observable units: ITutorialUnit[] = [];
//observable for loading indicator
#observable loadingInitial = false;
#action loadUnits = async () => {
//start the loading indicator
this.loadingInitial = true;
try {
//we use await to block anything block anything below list() method
const units = await agent.TutorialUnits.list();
units.forEach((unit) => {
this.units.push(unit);
// console.log(units);
});
this.loadingInitial = false;
} catch (error) {
console.log(error);
this.loadingInitial = false;
}
};
}
export default createContext(new UnitStore());
then I call this in my App component:
import React, { Fragment, useContext, useEffect } from "react";
import { Container } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import NavBar from "../../features/nav/NavBar";
import { ActivityDashboard } from "../../features/Units/dashboard/tutorialUnitDashboard";
import UnitStore from "../stores/unitStore";
import { observer } from "mobx-react-lite";
import { LoadingComponent } from "./LoadingComponent";
const App = () => {
const unitStore = useContext(UnitStore);
useEffect(() => {
unitStore.loadUnits();
//need to specify the dependencies in dependenciy array below
}, [unitStore]);
//we are also observing loading initial below
if (unitStore.loadingInitial) {
return <LoadingComponent content="Loading contents..." />;
}
return (
<Fragment>
<NavBar />
<Container style={{ marginTop: "7em" }}>
<ActivityDashboard />
</Container>
</Fragment>
);
};
export default observer(App);
Finally, I want to use this component to display my content:
import { observer } from "mobx-react-lite";
import React, { Fragment, useContext } from "react";
import { Button, Item, Label, Segment } from "semantic-ui-react";
import UnitStore from "../../../app/stores/unitStore";
const UnitList: React.FC = () => {
const unitStore = useContext(UnitStore);
const { units } = unitStore;
console.log(units)
return (
<Fragment>
{units.map((unit) => (
<h2>{unit.content}</h2>
))}
</Fragment>
);
};
export default observer(UnitList);
I can't see the units..
Where's the problem? My API is working, I tested with Postman.
Thanks!!
If you were using MobX 6 then you now need to use makeObservable method inside constructor to achieve same functionality with decorators as before:
class UnitStore {
#observable units: ITutorialUnit[] = [];
#observable loadingInitial = false;
constructor() {
// Just call it here
makeObservable(this);
}
// other code
}
Although there is new thing that will probably allow you to drop decorators altogether, makeAutoObservable:
class UnitStore {
// Don't need decorators now anywhere
units: ITutorialUnit[] = [];
loadingInitial = false;
constructor() {
// Just call it here
makeAutoObservable(this);
}
// other code
}
More info here: https://mobx.js.org/react-integration.html
the problem seems to be the version, I downgraded my Mobx to 5.10.1 and my mobx-react-lite to 1.4.1 then Boom everything's fine now.

React state property undefined in function component using Context API

I am new to React's context API and hooks for function components. I am trying to pass state to a child component, ActivityTable.js. I wrapped the provider around the app (App.js), however state properties are still undefined in ActivityTable.js -- TypeError: Cannot read property 'id' of undefined.
Any guidance would be appreciated.
App.js
import ActivityState from "./context/activities/ActivityState";
const App = () => {
return (
<StylesProvider injectFirst>
<ContactState>
<ActivityState>
...
</ActivityState>
</ContactState>
</StylesProvider>
);
};
export default App;
ActivityState.js
import React, { useReducer } from 'react';
import ActivityContext from './ActivityContext';
import ActivityReducer from './ActivityReducer';
import { ADD_ACTIVITY, DELETE_ACTIVITY, SET_CURRENT_ACTIVITY } from '../types';
const ActivityState = props => {
const initialState = {
activities: [
{
id: 1,
activity_description: "a desc",
activity_name: "a",
},
{
id: 2,
activity_description: "b desc",
activity_name: "b",
},
{
id: 3,
activity_description: "c desc",
activity_name: "c",
}
]
};
const [state, dispatch] = useReducer(ActivityReducer, initialState);
const deleteActivity = id => {
dispatch({ type: DELETE_ACTIVITY, payload: id });
};
const setCurrentActivity = activity => {
dispatch({ type: SET_CURRENT_ACTIVITY, payload: activity });
};
return (
<ActivityContext.Provider
value={{
activities: state.activities,
deleteActivity,
setCurrentActivity
}}>
{ props.children }
</ActivityContext.Provider>
);
}
export default ActivityState;
ActivityContext.js
import { createContext } from "react";
const ActivityContext = createContext(null);
export default ActivityContext;
ActivityReducer.js
import { DELETE_ACTIVITY, SET_CURRENT_ACTIVITY } from '../types';
export default (state, action) => {
switch (action.type) {
case DELETE_ACTIVITY:
return {
...state,
activities: state.activities.filter(
activity => activity.id !== action.payload
)
};
case SET_CURRENT_ACTIVITY:
return {
...state,
current: action.payload
};
default:
return state;
}
};
ActivityView.js
import React, { useContext } from "react";
import ActivityContext from '../../context/activities/ActivityContext';
import ActivityTable from './ActivityTable';
const Activities = () => {
const activityContext = useContext(ActivityContext);
const { activities } = activityContext;
console.log('activities: ', activities);
return (
<div>
<ActivityTable/>
</div>
);
}
export default Activities;
ActivityTable.js
import React, { useContext, useState } from "react";
import ActivityContext from "../../context/activities/ActivityContext";
const ActivityTable = ({ activity }) => { //activity is undefined here
const activityContext = useContext(ActivityContext);
const { activities } = activityContext;
const { id, activity_name, activity_desc } = activity; //undefined
return (
<div>
<tr>
<td>{id}</td>
<td>{activity_name}</td>
<td>{activity_desc}</td>
</tr>
</div>
);
};
export default ActivityTable;
It looks like you're using activity as a prop inside ActivityTable, but never actually supplying that prop.
<ActivityTable activity={foo} />
I can't tell what data you're trying to pass to the table. You're importing the context successfully in both components, but never using the context data.

Test React Component using Redux and react-testing-library

I am new to testing redux connected components in React and trying to figure out how to test them.
Currently I'm using react-testing-library and having trouble setting up the my renderWithRedux function to correctly setup redux.
Here is a sample component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Sample extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentDidMount() {
//do stuff
console.log(this.props)
}
render() {
const { user } = this.props
return(
<div className="sample">
{user.name}
</div>
)
}
}
const mapStateToProps = state => ({
user: state.user
})
export default connect(mapStateToProps, {})(Sample);
Here is a sample test:
import React from 'react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup } from 'react-testing-library';
import Sample from '../components/sample/'
const user = {
id: 1,
name: "John Smith"
}}
function reducer(state = user, action) {
//dont need any actions at the moment
switch (action.type) {
default:
return state
}
}
function renderWithRedux(
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
}
}
afterEach(cleanup)
test('<Sample> example text', () => {
const { getByTestId, getByLabelText } = renderWithRedux(<Sample />)
expect(getByText(user.name))
})
The user prop value always results as undefined. I have re-written this a couple of ways but can't seem to get it to work. If I pass the user data directly as a prop to Sample component in the test, it still resolves to be undefined.
I am learning from the tutorials and examples via the offical docs, like this one: https://github.com/kentcdodds/react-testing-library/blob/master/examples/tests/react-redux.js
Any pointers, tips or solutions would be greatly appreciated!
You should wrap the component inside Provider, here is the simple example
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import TestedComponent from '../index';
const mockStore = configureMockStore();
const store = mockStore({});
const renderTestedComponent = () => {
return render(
<Provider store={store}>
<TestedComponent />
</Provider>
);
};
describe('test TestedComponent components', () => {
it('should be render the component correctly', () => {
const { container } = renderTestedComponent();
expect(container).toBeInTheDocument();
});
});
**Unable to Fire event using #testing-library**
// demo.test.js
import React from 'react'
import { Provider } from "react-redux";
import '#testing-library/react/cleanup-after-each'
import '#testing-library/jest-dom/extend-expect'
import { render, fireEvent } from '#testing-library/react'
// this is used to fire the event
// import userEvent from "#testing-library/user-event";
//import 'jest-localstorage-mock';
import ChangePassword from './ChangePassword';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
test('test 1-> Update User password', () => {
// global store
const getState = {
authUser :{
user : {
email: "test#gmail.com",
id: 0,
imageURL: null,
name: "test Solutions",
roleId: 1,
roleName: "testRole",
userName: "testUserName"
},
loading: false,
showErrorMessage: false,
errorDescription: ""
}
}; // initial state of the store
// const action = { type: 'LOGIN_USER' };
// const expectedActions = [action];
// const store = mockStore(getState, expectedActions);
const onSaveChanges = jest.fn();
const changePassword = jest.fn();
const store = mockStore(getState);
const { queryByText, getByLabelText, getByText , getByTestId , getByPlaceholderText, } = render(
<Provider store={store}>
<ChangePassword
onSaveChanges={onSaveChanges}
changePassword={changePassword}
/>
</Provider>,
)
// test 1. check the title of component
expect(getByTestId('updateTitle')).toHaveTextContent('Update your password');
// test 2. chekck the inputfile
expect(getByPlaceholderText('Old Password')) //oldpassword
expect(getByPlaceholderText('New Password')) //newpassword
expect(getByPlaceholderText('Confirm New Password')) //confpassword
// change the input values
fireEvent.change(getByPlaceholderText("Old Password"), {
target: { value: "theOldPasword" }
});
fireEvent.change(getByPlaceholderText("New Password"), {
target: { value: "#Ab123456" }
});
fireEvent.change(getByPlaceholderText("Confirm New Password"), {
target: { value: "#Ab123456" }
});
// check the changed input values
expect(getByPlaceholderText('Old Password').value).toEqual("theOldPasword");
expect(getByPlaceholderText('New Password').value).toEqual("#Ab123456");
expect(getByPlaceholderText('Confirm New Password').value).toEqual("#Ab123456");
expect(getByText('Save Changes')); // check the save change button
// calling onSave function
//fireEvent.click(getByTestId('savechange'))
// userEvent.click(getByText('Save Changes'));
})

React Redux - store updates, component re-renders... but not with new data

I have a complicated App with a problem which I have reduced down to 3 files below. Essentially, what is happening is:
component loads and render initial text
a spoof API is then triggered which calls this.props.route.onLoadData()
this in turn calls a reducer which returns a new object containing updated text
the component re-renders accordingly, but not with the updated text
I'm using <Provider/> and connect() - neither succeeds.
Any ideas?
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, hashHistory } from 'react-router'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import Reducers from './reducers'
import Problem from './Problem'
var store = createStore(Reducers)
var routes = [
{ path: '/problem',
component: Problem,
data: store.getState(),
onLoadData: (res) => store.dispatch({ type: 'LOAD_DATA', data: res })
}
]
var render = function() {
return ReactDOM.render(
<Provider store={store}>
<Router history={hashHistory} routes={routes}/>
</Provider>,
document.getElementById('app')
)
}
render()
store.subscribe(render)
problem.js
import React from 'react'
import { connect } from 'react-redux'
var ProblemContainer = React.createClass({
spoofAPI() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ text: 'updated text' })
}, 1000)
})
},
componentDidMount() {
this.spoofAPI().then((res) => {
return this.props.route.onLoadData( res )
}, console.error)
},
render() {
var text = this.props.route.data.text
console.log('render:', text)
return (
<div>{text}</div>
)
}
})
const mapState = state => ({ text: state })
export default connect(mapState)(ProblemContainer)
reducers.js
export default (state = { text: 'initial text' }, action) => {
switch (action.type) {
case 'LOAD_DATA':
var newState = Object.assign({}, state)
console.log('reducer - existing state: ', newState.text)
newState.text = action.data.text
console.log('reducer - receives: ', action.data.text)
var returnObject = Object.assign({}, state, newState)
console.log('reducer - returnObject: ', returnObject)
return returnObject
default:
return state
}
}
According to these two pieces of code:
var routes = [
...
data: initialState,
...
]
and
render() {
var text = this.props.route.data.text
...
}
It looks like what you are doing is always referencing one and the same piece of data (which is initialState) without even touching store.
UPDATE. What you have to do is to connect your Component to store via react-redux and use store values in your Component.
import React from 'react'
import { connect } from 'react-redux'
const Foo = React.createClass({
...
render() {
var text = this.props.text
console.log('render:', text)
return (
<div>{text}</div>
)
}
})
const mapState = state => ({ text: state });
export default connect(mapState)(Foo);
Adding an onGetState() which can be called from the component works... but is this an anti-pattern of sorts (?)
index.js
var routes = [
{ ...
onGetState: () => store.getState()
...
}
]
problem.js
render() {
var { text } = this.props.route.onGetState()
...
}

Categories

Resources