Access stores from class using mobX and react Context - reactjs

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"
}
}

Related

React - useEffect based on service variable

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>
})

How to Call a Function From Another Class With React-Redux?

I want to call functions from another class in react.Js and I did it successfully. My problem is that I get Error: You must pass a component to the function returned by connect. Instead received {"refs":{},"updater":{}} if I use react-redux. Here is a basic example of algortihm
import { Component } from "react";
import { GetCityListFromActiveAddressSource } from '../Services/AddressService'
import { connect } from 'react-redux';
class FirstClass extends Component{
constructor(props){
super(props);
}
TestFunction(){
alert("test");
}
render(){
return null
}
}
const mapStateToProps = ({ Modules }) => {
const { GetDynamicMenuList } = Modules;
return { GetDynamicMenuList }
}
const Address = new AddressService();
export default connect(mapStateToProps,{GetCityListFromActiveAddressSource})(Address);
//export default Address;
And My Second class...
import React, { Component } from 'react';
import FirstClass from '../../../ServiceClient/FirstClass';
class SeconClass extends Component {
constructor(props) {
super(props);
}
componentDidMount(){
Address.TestFunction();
}
}
If I don't use connect and react-redux then I can call my function. But I have to use react-redux.
I solved my problem, My problem was that I used export default with const value but react-redux(connect) needs a component or function But I unfortunatelly used const value in my example. That's why I got error. Then I exported my component in connect and I used another export for my const. It works well enough for me.
import React, { Component } from "react";
import { GetCityListFromActiveAddressSource } from '../Services/AddressService'
import { connect } from 'react-redux';
class AddressService extends Component{
constructor(props){
super(props);
}
TestFunction(){
alert("test");
}
render(){
return null
}
}
const mapStateToProps = ({ Modules }) => {
const { GetDynamicMenuList } = Modules;
return { GetDynamicMenuList }
}
export default connect(mapStateToProps,{GetCityListFromActiveAddressSource})
(AddressService);
export const Address = new AddressService();
And I call my function like
import {Address} from '../../../ServiceClient/Address';

Typescript constraints with redux connect

Using typescript for react as the language i have declared a class with constraints . I need to apply the connect method to it
import * as React from 'react';
import { connect } from 'react-redux';
import { initAskadeFiles } from '../Entities/Askade/Askade.Actions';
import { Dispatch } from 'redux';
interface IProp<T> {
PropOne: T
}
interface IState<T> {
StateOne: T
}
class BaseEdit<T> extends React.Component<IProp<T>, IState<T>> {
}
export function mapDispatchToProps(dispatch: Dispatch, ownProps: any) {
return {
InsertItem: () => dispatch(initAskadeFiles())
}
}
export default connect(null, mapDispatchToProps)(BaseEdit);
And in the calling component below is the syntax
import * as React from 'react';
import BaseEditSample from './BaseEditSample';
import { City } from '../Components/GridFunctionality/City';
export class ComplexEditSample extends React.Component {
public render(): any {
<BaseEditSample<City> />
}
}
When i use the syntax with City being passed to it i get an error
what am i missing in this i need redux to be connected to this component along with the contraints? Thanks
[ts] Expected 0 type arguments, but got 1.
First, you can pass your actions directly to the connect without mapDispatchToProps like this:
export default connect(mapStateToProps, { state, actions })(Component);`
And in your component use the imported actions and states:
type ComponentProps = {state, actions};
export default class Component extends React.Component<ComponentProps> {
render() {
const {
data,
actions,
} = this.props
return (...)
}

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.

How can I extend a "connected" component?

My situation is, I have the Navigation component, which is the base, and is listening to the Navigations state(Redux). It should be extended to HorizontalNavigation and VerticalNavigation, for easy reusable code in the future.
My problem is, right now I already have the "final" version of Navigation.jsx and I can extend it, as a class, but can't override it's methods. It triggers the super(Navigation) method and not the final one. I need to override the methods in Horizontal or Vertical components.
There is no code erros on console, so it isn't something breaking, but that I don't know how to handle how to extend it.
Navigation.jsx
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { itemAction, stageAction } from 'Store/Actions/Actions';
class Navigation extends Component {
// ACTIONS
leftAction () {
this.onLeftAction();
}
onLeftAction () {}
rightAction () {
this.onRightAction();
}
onRightAction () {}
downAction () {
this.onDownAction();
}
onDownAction () {}
upAction () {
this.onUpAction();
}
onUpAction () {}
// STAGES
nextStage (slug) {
this.goToStage(slug);
}
previousStage (slug) {
this.goToStage(slug);
}
goToStage (slug) {
// Just for illustration purpose
// let { dispatch } = this.props;
// dispatch(stageAction(slug));
}
// ITEMS
nextItem (index) {
this.goToItem(index);
}
previousItem (index) {
this.goToItem(index);
}
goToItem (index) {
// Just for illustration purpose
// let { dispatch } = this.props;
// dispatch(itemAction(index));
}
render () {
return ();
}
}
function mapStateToProps (state, props) {
navigation: state.Navigations[props.slug]
}
export default connect(mapStateToProps)(Navigation);
Horizontal.jsx
import React from 'react';
import Navigation from 'Components/Navigation/Navigation';
class HorizontalNavigation extends Navigation {
onLeftAction (previousItemIndex) {
this.previousItem(previousItemIndex);
}
onRightAction (nextItemIndex) {
this.nextItem(nextItemIndex);
}
onTopAction (slug) {
this.previousStage(slug);
}
onDownAction (slug) {
this.nextStage(slug);
}
}
export default HorizontalNavigation;
The VerticalNavigation would be the opposite. Left and right for stage; up and down for items.
I don't want to reuse the Navigation component each time I could use Horizontal or Vertical, and rewrite the same exact logic over and over again.
I'm using the Higher-Order Component pattern, exporting a function to connect the extended component, eg:
import { connect as reduxConnect } from 'react-redux'
...
export class Navigation extends Component{
...
export function connect(Component){
return reduxConnect(
(state, props)=>({...})
)(Component);
}
export default connect(Navigation)
And in the Horizontal.jsx you could do
import { Navigation, connect } from './Navigation';
class Horizontal extends Navigation{
...
export default connect(Horizontal);
This way, you keep the connect(mapStateToProps) in one place.
This is a fun one. At the bottom of Navigation, you're exporting the connecting component, which in essence is exporting the class created in connect, which is not the same class as Navigation. So, when you extend the default exported class, you're actually extending the connect class. That's a mouthful.
To get this to work, you could also export your class (in addition to export default connect(mapStateToProps)(Navigation); at the bottom:
export class Navigation extends Component {
Then to extend it, you can do:
import { Navigation } from './Navigation';
class Horizontal extends Navigation {
// ...
However, you would also need connect the Horizontal component as well in order to get the right props from redux.
If you don't want to use connect, you could take in props to your Navigation component that changes how those up/down/left/right actions work, then you could create a Horizontal/Vertical component that passes in the right props. Something like:
class Horizontal extends React.Component {
constructor(props, context) {
super(props, context);
this.onUp = this.onUp.bind(this);
this.onDown = this.onDown.bind(this);
this.onLeft = this.onLeft.bind(this);
this.onRight = this.onRight.bind(this);
}
onUp() {}
onDown() {}
onLeft() {}
onRight() {}
render() {
return (
<Navigation onUp={this.onUp} onDown={this.onDown} onLeft={this.onLeft} onRight={this.onRight} />
);
}
);

Resources