React - useEffect based on service variable - reactjs

I have a question about usage of React.useEffect function based on the variable being a part of a service which I use to make some magic behind.
import React, { useEffect } from "react";
import { treeStructureService } from "../../services/dependency_injection";
import "./ModelTree.css";
const ModelTree = (props: any) => {
useEffect(() => {
// some code
console.log('Use Effect runs...')
}, [treeStructureService.tree])
return <div>ModelTree</div>;
};
export { ModelTree };
TreeStructureService.tree changes the variable depending upon the upload of new files to a project. Such action takes some time in the background, which is why I tried to use such a variable in useEffect to rerender the tree again when changes were propagated to the service.
The most interesting part of the TreeStructureService was presented below:
import { TreeNode } from "../../interfaces/tree_node";
import { modelLoaderService } from "../dependency_injection";
export class TreeStructureService {
public tree: TreeNode | undefined;
constructor() {}
async addTreeToProject(modelID: number, newTree: TreeNode):Promise<void> {
if (modelID == 0) {
this.tree = newTree;
}else{
console.log('Doing magic')
}
}
}
In dependency injection, necessary services are called and exported to use the "equivalent" of DependencyInjection from Angular.:
import { IFCLoaderService } from "./viewer/model_loaders/ifc_loader_service";
import { ModelLoaderService } from "./viewer/model_loaders/model_loader_service";
import { SelectionService } from "./viewer/selection_service";
import { ThreeSceneService } from "./viewer/three_scene_service";
import { TreeStructureService } from "./viewer/tree_structure_service";
import { VisibilityService } from "./viewer/visiblity_service";
export const modelLoaderService = new ModelLoaderService();
export const ifcLoaderService = new IFCLoaderService();
export const threeSceneService = new ThreeSceneService();
export const selectionService = new SelectionService();
export const visibilityService = new VisibilityService();
export const treeStructureService = new TreeStructureService();
I'll be glad for any suggestions. In the next steps, I'll add redux to control the state of the application. So maybe you have some idea that I could pass a new tree as an action argument? However, I don't know how to do it outside of the components.

While you don't need any fancy code to connect your tree model to React, there a few ways to do that.
Basically, you have to wire or connect your state changes.
You could write your own event emmitter, then subscribe via react hook, but here is straightforward shortcut. Mobx does this for you
import React, { useEffect } from "react"
import { treeStructureService } from "../../services/dependency_injection"
import "./ModelTree.css"
import { TreeNode } from "../../interfaces/tree_node"
import { modelLoaderService } from "../dependency_injection"
// Step 1: Notice 2 new imports
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react-lite"
export class TreeStructureService {
public tree: TreeNode | undefined
constructor() {
// Step 2: notice that I mark `tree` as observable
makeAutoObservable(this)
}
async addTreeToProject(modelID: number, newTree: TreeNode): Promise<void> {
if (modelID == 0) {
this.tree = newTree
} else {
console.log("Doing magic")
}
}
}
// Step 3: Notice the observer wrapper from "mobx-react-lite"
export const ModelTree = observer((props: any) => {
// This re-render when TreeNode changes
console.log(treeStructureService.tree)
return <div>ModelTree</div>
})

Related

React Sortable JS with mobx

I am building a functional component that should render a sortable component (ReactSortable) with computed array (layers array) that comes from a mobx store using react-sortablejs. The component renders when the apps load, but when resorting the array by dragging and dropping elements, I get the following error:
mobx.module.js:89 Uncaught Error: [mobx] Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an action if this change is intended. Tried to modify: LayerStore#23.layersRegistry.9f332395-6bfe-4c3f-a496-8804e1dec490.chosen?
Actually, the layers array is computed from layersRegistry Map observable in the store as the code below shows, but when sorting the layers, I am not changing the layersRegistry observable; that’s why I got confused from the error.
Whats the problem in this case, and how can I resolve it such that when I resort the layers (by drag drop) the layers variable in the store or the layersRegistry Map observable is updated based on the sorting?
Here the code for the component
import React, { useContext, useEffect, Fragment, useState } from "react";
import { RootStoreContext } from "../../../app/stores/rootStore";
import { observer } from "mobx-react-lite";
import LayerListItem from "./LayerListItem";
import { ReactSortable } from "react-sortablejs";
export const LayersDashboard = () => {
const rootStore = useContext(RootStoreContext);
const { layers, loadLayers } = rootStore.layerstore;
useEffect(() => {
loadLayers();
}, [loadLayers]);
const [items, setItems] = useState(layers);
return (
<Fragment>
<ReactSortable list={layers} setList={setItems}>
{layers.map((layer) => (
<LayerListItem key={layer.id} layer={layer} />
))}
</ReactSortable>
</Fragment>
);
};
export default observer(LayersDashboard);
Here is the code for the store
import { observable, action, runInAction, computed } from "mobx";
import agent from "../api/agent";
import { RootStore } from "./rootStore";
import { ILayer } from "../models/Layer";
export default class LayerStore {
rootStore: RootStore;
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
}
#observable layersRegistry = new Map();
#computed get layers() {
return Array.from(this.layersRegistry.values());
}
#action loadLayers = async () => {
try {
const layers = await agent.Layers.list();
runInAction(() => {
layers.forEach((layer) => {
this.layersRegistry.set(layer.id, layer);
});
});
} catch (error) {
runInAction(() => {
console.log("error");
});
}
};
}
I would suggest to call toJS on your data before passing it to ReactSortable or any other library and to keep your observable data in sync with sortable data you will need to use some onDragEnd event hook and call mobx action from there to update observable data. But I'm not sure if react-sortablejs supports this event hook.

Why does the observer not react on updates in my store?

I'm trying to get mobx to work with my react hooks and I am starting off with a very simple example but it still does not work. I must have missed something but I have been trying for hours to find what that might be.
Here is my store:
import { observable, decorate } from 'mobx';
import { createContext } from 'react';
import { IUser } from '../models/IUser';
export class UserStore {
public user: IUser;
public getUser() {
myApi
.get(myUrl)
.then(response => {
this.user = response;
});
}
}
decorate(UserStore, {
user: observable,
});
export default createContext(new UserStore());
And here is the component printing the username of the user:
import React, { useContext } from 'react';
import { observer } from 'mobx-react-lite';
import UserStore from '../../stores/UserStore';
const MyComponent = observer(() => {
const userStore = useContext(UserStore);
return (
<div>
{userStore.user && userStore.user.userName}
</div>
);
});
export default MyComponent;
And to fire the api call, App does the following:
const App: React.FC = () => {
const userStore = useContext(UserStore);
useEffect(() => {
userStore.getUser();
}, []);
return(...);
};
export default App;
I can see that
1: App performs the call
2: The user is set to the response of the call
3: If I console log the userStore.user.userName after it has been set, it looks just fine.
The quirk is that the label in MyComponent never gets updated. Why?
I believe the bug is in decorate.
Changing the behavior from using the decorate syntax, to wrapping the FC with observable works just fine, like this:
const UserStore = observable({
user: {}
});
Another thing that also works is to have your stores as classes and using the old decorator syntax like this:
export class UserStore {
#observable public user: IUser;
}
This requires you to add the following to .babelrc:
["#babel/plugin-proposal-decorators", { "legacy": true }]

Access stores from class using mobX and react Context

I am strugling a bit with mobx/mobx-react-lite and react hooks.
From a class i want to update a property in one of my stores, but somehow i cant get it to work. Here are some examples of how my stores are combined, and the component and class i want to call my store from. I am using Context from react to get the stores in my hook component, and that works perfectly.
// FooStore
import { observable, action } from "mobx";
import ExampleClass from "app/services/exampleClass";
export class FooStore {
#observable
public foo: string = "";
#action
public doSomething() {
this.foo = ExampleClass.doSomething()
}
}
export default FooStore;
// BarStore
import { observable, action } from "mobx";
export class BarStore {
#observable
public bar: number = 0;
#action
public setBar(value: number) {
this.bar
}
}
export default BarStore;
//Store (Combining the stores to one, and exporting with createContext())
import { FooStore } from "./FooStore";
import { BarStore } from "./BarStore";
import { createContext } from "react";
class Store {
public fooStore: FooStore;
public barStore: BarStore;
constructor(){
this.fooStore = new FooStore();
this.barStore = new BarStore();
}
}
const stores = new Store()
export default createContext(stores);
This is the class i want to be able to call my barStore. (Notice, not a component class)
//ExampleClass
export default class ExampleClass {
public static doSomething(): string {
// ...
// Call BarStore.setBar(1000)
return "Some string"
}
}
Can anyone push me in the right direction for this?
Context is a React concept. it's not good to export your store by Context. (May be you should need to use it in another environment !) You should export store itself and wrap it through context in your highest level component.
//Your store:
import { FooStore } from "./FooStore";
import { BarStore } from "./BarStore";
class Store {
public fooStore: FooStore;
public barStore: BarStore;
constructor(){
this.fooStore = new FooStore();
this.barStore = new BarStore();
}
}
const stores = new Store()
export default stores;
//App.js ...
import store from './yourStore';
import { createContext } from "react";
const GlobalStore = createContext(store);
export default () => {
<GlobalStore.Provider>
<Main />
</GlobalStore.Provider>
}
//Any other js file
import store from './yourStore';
export default class ExampleClass {
public static doSomething(): string {
// ...
store.BarStore.setBar(1000)
return "Some string"
}
}

How to import tsx into a SPFx extension

I have just started my first SharePoint project and cannot figure out how to use my React components in my extension. Here are the relevant files.
Navbar.tsx:
import * as React from "react";
export const Navbar = props => <div>Hello world</div>;
ReactSharePointNavbarApplicationCustomizer.tsx:
import { override } from "#microsoft/decorators";
import { Log } from "#microsoft/sp-core-library";
import {
BaseApplicationCustomizer,
PlaceholderContent,
PlaceholderName
} from "#microsoft/sp-application-base";
import { Dialog } from "#microsoft/sp-dialog";
import * as strings from "ReactSharePointNavbarApplicationCustomizerStrings";
import styles from "./AppCustomizer.module.scss";
import { escape } from "#microsoft/sp-lodash-subset";
import * as Components from "./components";
import Navbar = Components.Navbar;
const LOG_SOURCE: string = "ReactSharePointNavbarApplicationCustomizer";
/**
* If your command set uses the ClientSideComponentProperties JSON input,
* it will be deserialized into the BaseExtension.properties object.
* You can define an interface to describe it.
*/
export interface IReactSharePointNavbarApplicationCustomizerProperties {}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class ReactSharePointNavbarApplicationCustomizer extends BaseApplicationCustomizer<
IReactSharePointNavbarApplicationCustomizerProperties
> {
private _onDispose(): void {
console.log("No place holder.");
}
private _topPlaceholder: PlaceholderContent | undefined;
private _renderPlaceHolders(): void {
if (!this._topPlaceholder) {
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
{ onDispose: this._onDispose }
);
if (!this._topPlaceholder) {
return;
}
if (this.properties) {
const Nav = Navbar(null);
if (this._topPlaceholder.domElement) {
this._topPlaceholder.domElement.innerHTML = `
<div class="${styles.app}">
<div class="ms-bgColor-themeDark ms-fontColor-white ${
styles.top
}">
${Nav}
${Navbar}
<div>Hello</div>
<Navbar/>
</div>
</div>`;
}
}
}
}
#override
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);
// Added to handle possible changes on the existence of placeholders.
this.context.placeholderProvider.changedEvent.add(
this,
this._renderPlaceHolders
);
// Call render method for generating the HTML elements.
this._renderPlaceHolders();
return Promise.resolve<void>();
}
}
components:
export * from "./Navbar";
My goal is to use my react component as a navigation bar, however I cannot manage to combine tsx and ts in this context.
I followed this guide: https://learn.microsoft.com/en-us/sharepoint/dev/spfx/extensions/get-started/using-page-placeholder-with-extensions
Outside of these files, the only modifications I made were to add a components folder, with the component and index you see above.
Please help me solve this challenge.
After working on this for a few hours, I have found the solution. I was coming at this the wrong way, I needed to use ReactDOM to insert my TSX components. Afterward it was normal React development. No need to try to insert elements in some fancy way as I was doing before.
Here is the working code.
ReactSharePointNavbarApplicationCustomizer.ts:
import * as React from "react";
import * as ReactDOM from "react-dom";
import { override } from "#microsoft/decorators";
import { Log } from "#microsoft/sp-core-library";
import {
BaseApplicationCustomizer,
PlaceholderContent,
PlaceholderName
} from "#microsoft/sp-application-base";
import { Dialog } from "#microsoft/sp-dialog";
import * as strings from "ReactSharePointNavbarApplicationCustomizerStrings";
import styles from "./AppCustomizer.module.scss";
import { escape } from "#microsoft/sp-lodash-subset";
import Navbar, { INavbarProps } from "./components/Navbar";
const LOG_SOURCE: string = "ReactSharePointNavbarApplicationCustomizer";
export interface IReactSharePointNavbarApplicationCustomizerProperties {}
export default class ReactSharePointNavbarApplicationCustomizer extends BaseApplicationCustomizer<
IReactSharePointNavbarApplicationCustomizerProperties
> {
private _onDispose(): void {}
private onRender(): void {
const header: PlaceholderContent = this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
{
onDispose: this._onDispose
}
);
if (!header) {
Log.error(LOG_SOURCE, new Error("Could not find placeholder PageHeader"));
return;
}
const elem: React.ReactElement<INavbarProps> = React.createElement(Navbar);
ReactDOM.render(elem, header.domElement);
}
#override
public onInit(): Promise<void> {
this.onRender();
return Promise.resolve<void>();
}
}
Navbar.tsx:
import * as React from "react";
import styles from "./Navbar.module.scss";
import NavbarItem from "../NavbarItem";
export interface INavbarProps {}
export default class Navbar extends React.Component<INavbarProps> {
constructor(props: INavbarProps) {
super(props);
}
public render(): JSX.Element {
return (
<div className={"ms-bgColor-themeDark ms-fontColor-white " + styles.nav}>
Hello world
</div>
);
}
}
As you can see, the components.ts export file was unnecessary. And I am sure other code may still be useless in these examples.
I found that importing tsx components into other tsx components works like normal React imports. Just import and insert as an element.

Get redux state on window.scroll

I want to implement pagination. So when a user scrolls down to the bottom I want to make an api call. I see through window.scroll I can find position of scroll and can achieve that. However I want to access redux state to get certian data. Since this event is not bind by any component I won't be able to pass down data. What would be the correct approach in this scenario?
If I want to access redux store through a simple function How can I do that? And on scroll how do I make sure that only request goes through?
You can connect your component that does the scroll. or you can pass props to the component that have the store information. Those are the two recommended ways to reach your store. That being said you can also look at the context
class MyComponent extends React.Component {
someMethod() {
doSomethingWith(this.context.store);
}
render() {
...
}
}
MyComponent.contextTypes = {
store: React.PropTypes.object.isRequired
};
Note: Context is opt-in; you have to specify contextTypes on the component to get it.
Read up on React's Context doc It may not be a complete solution since it could be deprecated in a future version
Edit:
Per the comments with the clarity you provided you can just do this.
import React, { Component } from 'react';
import ReactDOM = from 'react-dom';
import _ from 'lodash';
const defaultOffset = 300;
var topOfElement = function(element) {
if (!element) {
return 0;
}
return element.offsetTop + topOfElement(element.offsetParent);
};
class InfiniteScroll extends Component {
constructor(props) {
super(props);
this.listener = _.throttle(this.scrollListener, 200).bind(this);
}
componentDidMount() {
this.attachScrollListener();
}
componentWillUnmount() {
this.detachScrollListener();
}
scrollListener () {
var el = ReactDOM.findDOMNode(this);
var offset = this.props.offset || defaultOffset;
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
if (topOfElement(el) + el.offsetHeight - scrollTop - window.innerHeight < offset) {
this.props.somethingHere;
}
}
attachScrollListener() {
window.addEventListener('scroll', this.listener);
window.addEventListener('resize', this.listener);
this.listener();
}
detachScrollListener() {
window.removeEventListener('scroll', this.listener);
window.removeEventListener('resize', this.listener);
}
render() {
return (...)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(InfiniteScroll);
I added lodash to the import here so you can throttle the scroll listener function. you only want to call the handler function so many times a second or it can start lagging the page (depending on how heavy the listener function is)
The correct way to access your application state in components is the usage of react-redux and selectors functions.
react-redux provides a function which is called connect. You should use this function to define which values from our state you want to map to the props of the component so these will be available.
The function you need for this mapping is called mapStateToPropswhich returns an object with the values which should be passed to the component.
Also you can be define redux actions which should be made available in the component (e.g. for trigger the load of the next page). The function is called mapDispatchToProps.
Here an example:
import React from 'react';
import { connect } from 'react-redux';
import { getUsersPage } from './selectors';
import { loadUsersPage } from './actions';
class MyComponent extends React.Component {
handleScroll () {
this.props.loadUsersPage({ page: lastPage + 1 });
}
render () {
const users = this.props.users;
// ...
}
}
const mapStateToThis = (state) => {
return {
users: getUsers(state)
};
}
const mapDispatchToProps = (dispatch) => {
return {
loadUsersPage: (payload) => {
dispatch (loadUsersPage(payload));
}
}
};
export default connect()(MyComponent);

Resources