Today I started using MobX and the first problem I ran into is how to execute a function in a React class component whenever an Observable updates.
I am under the impression this can be achieved using a reaction, but I'm not sure how to make it work.
class MissionLog {
private _missions: Array<IMissionItem> = [];
public get missions() {
return this._missions;
}
constructor() {
makeAutoObservable(this);
}
// Example of a method that modifies the _missions array
public receiveMission(mission: IMissionItem) {
this._missions.push(mission);
}
}
export const missionLog = new MissionLog();
// Example of modifying the missions array
missionLog.receiveMission(someMission);
export const ObserverTest = observer(class _ObserverTest extends React.Component {
constructor(props: any) {
super(props);
// Executes the console.log at the start,
// but not when missionLog.missions changes.
autorun(() => {
console.log("Autorun", missionLog.missions);
})
// Never executes the console.log
reaction(
() => missionLog.missions,
(mission) => {
console.log("Reaction");
}
)
}
render() {
return (
// Accessing missionLog.missions here
// gives me the correct, updated data,
// so my setup should be fine.
)
}
});
I also tried to use intercept and observe instead of reaction, but also no result.
Related
I am not sure how to frame this question properly but I will try my best to help you understand my problem. I am little bit new to frontend so facing some difficulties. There is a controller called TableViewCotroller which is written in typescript and also includes JSX code. This controller is responsible for performing any table related operations. This controller looks like this:
import * as React from 'react';
export class TableViewController extends BaseViewController {
constructor(props: any) {
super(props);
}
protected onInsertRow = async (arg: InsertArgument) => {
//some row insertion logic
};
protected onDeleteRow = async (arg: InsertArgument) => {
//some row deletion logic
};
protected onInsertColumn = async (arg: InsertArgument) => {
//some column insertion logic
};
protected onDeleteColumn = async (arg: InsertArgument) => {
//some Column deletion logic
};
render()
{
const {//some properties} = this.props
const {//state1, state2 etc} = this.state
return (
<Table
onInsertRow={this.onInsertRow}
onDeleteRow={this.onDeleteRow}
onInsertColumn={this.onInsertColumn}
onDeleteColumn={this.onDeleteColumn}
...//some other properties like this
/>
);
}
Now I have decided to decouple some of these commands like onInsertRow, onInsertColumn etc from TableViewController and move them to TableCommandingController.
My TableCommandingController looks like this :
export class TableCommanding implements ITableCommandingController{
constructor(props: any) {
super(props);
}
protected onDeleteRow = async (arg: InsertArgument) => {
//some row deletion logic
};
protected onInsertColumn = async (arg: InsertArgument) => {
//some column insertion logic
};
protected onDeleteColumn = async (arg: InsertArgument) => {
//some Column deletion logic
};
//Some other commands
}
After creating TableCommandingController, I have added a variable in TableViewController called tableCommandingController. Something like below:
export class TableViewController extends BaseViewController {
private tableCommandingController?: TableCommandingController;
constructor(props: any) {
super(props);
}
//Rest of the class
}
Now I will initialize this tableCommandingController in async fashion. I don't want to initialize it in synchronous manner(there is some other requirement). So I created a function and I will initialize it in there. Now my TableViewController looks like this:
export class TableViewController extends BaseViewController {
private tableCommandingController?: TableCommandingController;
constructor(props: any) {
super(props);
}
protected getCommandingController = async () => {
if (this.tableCommandingController !== undefined) return this.tableCommandingController;
//Here I will lazy load TableCommandingModule using webpack magic comment
const {TableCommanding } = await import(/* webpackChunkName: "TableCommandingController" */ './TableCommandingController');
this.tableCommandingController = new TableCommanding(this.props);
return this.tableCommandingController;
}
}
Now when I have tableCommanding initialized, I wanted to use tableCommanding's onInsertRow, onInsertColumn etc functions inside render of TableViewController. But I have no idea how can I do that. I have tried something below but it was not working:
render()
{
const {//some properties} = this.props
const {//state1, state2 etc} = this.state
return (
<Table
onInsertRow={this.tableCommandingController?.onInsertRow}
onDeleteRow={this.tableCommandingController?.onDeleteRow}
onInsertColumn={this.tableCommandingController?.onInsertColumn}
onDeleteColumn={this.tableCommandingController?.onDeleteColumn}
...//some other properties like this
/>
);
}
In the above method my tableCommandingController is always uninitialized. So I know that I need to call getCommandingController() to get the initialized value of tableCommandingController. Something like below:
async render()
{
const commanding = await this.getTableCommandingController();
const {//some properties} = this.props
const {//state1, state2 etc} = this.state
return (
<Table
onInsertRow={commanding .onInsertRow}
onDeleteRow={commanding .onDeleteRow}
onInsertColumn={commanding .onInsertColumn}
onDeleteColumn={commanding .onDeleteColumn}
...//some other properties like this
/>
);
}
But I cannot make render function async. Any idea how can I do this?
Please keep your render method only to destructure props, state, any small sync operation and returning JSX. Render methods are not meant for async operations. Async operations needs to be handled through react lifecycle methods. Read up lifecycle methods here: https://reactjs.org/docs/state-and-lifecycle.html
You should ideally do this operation in componentDidMount and use state to re-render your component so that the callbacks are re-assigned. Else, without a re-render, your JSX wouldn't have the actual callback, instead would be undefined as that's what was rendered during mount.
I am accessing JSON file in ComponentDidMount in class A, i need to access that result outside class and need to use that in Class B
let test;
console.log(test);
class CustomerPage extends React.Component {
componentDidMount(): void {
$.getJSON("/api/LocaleStrings")
.done(results => {
let JsonString = JSON.parse(results);
test = new LocalizedStrings(JsonString);
})
.fail(console.log.bind(console));
}
}
Here, console.log(test) yields undefined.
It seems to me that your console.log(test) gets executed before the AJAX call returns, and at that point it will be uninitialized (undefined). Place your console.log inside the done function.
You could store your AJAX result in your component's state:
class CustomerPage extends React.Component {
constructor(props) {
super(props);
this.state = { test: null };
}
componentDidMount(): void {
$.getJSON("/api/LocaleStrings")
.done(results => {
let JsonString = JSON.parse(results);
this.setState({
test: new LocalizedStrings(JsonString);
});
})
.fail(console.log.bind(console));
}
}
You need to have an "event" that notifies anyone who is interested that test is available:
interface CustomerPageProps {
onLocaleStringsLoaded?: (test:object) => void
}
class CustomerPage extends React.Component<CustomerPageProps> {
static defaultProps {
onLocaleStringsLoaded: () => {} //nothing by default
}
componentDidMount(): void {
$.getJSON("/api/LocaleStrings")
.done(results => {
let JsonString = JSON.parse(results);
const test = new LocalizedStrings(JsonString);
this.props.onLocaleStringsLoaded(test);
}).fail(console.log.bind(console));
}
}
Then at some point in your code you could have:
<CustomerPage onLocaleStringsLoaded={window.console.log.bind(window.console)} />
which will print to the console once the result is available.
I recommend reading up a bit more on how React components share data. The component that needs the data can have an input defined, in which you can pass the test variable. Or using a redux store (which could potentially be a little too complex for your application). If you really want to continue this route. You can always use the window object to set a global variable: window.test = 'bla';. This is available anywhere in the application with console.log(window.test);.
You would have to update your code to:
window.test = new LocalizedStrings(JsonString);.
Verifying that it is set can be done with an interval:
setInterval(function() {
console.log(window.test);
}, 100);
Is a bad practice to have an object with internal state as an instance property in a React component?
for example
class PageCacher {
constructor(fetchMethod) {
this.fetchMethod = fetchMethod
this.pages = []
}
async getPage(page) {
if (this.pages[page]) {
return this.pages[page]
} else {
const result = await this.fetchMethod(page)
this.pages[page] = result
return result
}
}
}
class ItemList extends React.Component {
constructor(props) {
super(props)
this.pageCacher = new PageCacher(props.fetchServiceMethod)
}
hanldeFetchPage = (page) => {
this.pageCacher.getPage(page).then(result => {
this.setState({items: result})
})
}
}
PageCache keeps the pages requested stored and returns the result if present if not makes the service call.
Since you are initializing pageCacher in the constructor, it will only receive the props present at time of the component mounting.
This means that if any of the props change, pageCacher will NOT receive those updated props.
Link to docs on component constructor
I have 2 components, one is Grid component where I show list of some data. Another one is "functional" component which contains logic like sorting, paging, etc which will be in every Grid component. Grid component can contain some own specific logic also.
My question is how I can pass props to functional component whenever something was changed in Grid component. Right now it only pass props for the first time Grid component is initialized, after that functional component works with old props which are nevew actualized.
Grid component is connected to redux store but I dont think it does matter here.
I am not sure if initializing class component via state is good approach, is there another way how to do it?
Code to grid component
type ProcessListProps =
ProcessState.IProcessesStateType
& typeof ProcessState.actionCreators
& RouteComponentProps<{}>;
type State = {
sharedFunctionality: SharedGridFunctions;
}
class ProcessList extends React.Component<ProcessListProps, State> {
constructor(props) {
super(props);
this.state = {
sharedFunctionality: new SharedGridFunctions(this.props)
}
}
componentDidMount() {
this.props.requestData(this.props.dataQuery);
}
componentDidUpdate(oldProps): void {
if ((oldProps.dataQuery.context !== this.props.dataQuery.context)) {
this.props.requestData(this.props.dataQuery);
}
if (oldProps.sort !== this.props.sort) {
this.state.sharedFunctionality.buildSortExpression();
}
}
..render - removed for brevity
}
Code for Functional component
interface ISharedGridFunctions extends IGridBase, IBaseFilterActionCreator, IBaseActionCreator {
}
export default class SharedGridFunctions extends React.PureComponent<ISharedGridFunctions,{}>{
constructor(props) {
super(props);
}
componentDidUpdate() {
console.log("shared component updated");
}
pageChange = (event) => {
this.setPaging(event.page.skip, event.page.take);
};
setPaging = (skip: number, take: number) => {
this.props.setPaging(take, skip);
};
sortChange = (event) => {
const dataQuery = this.props.dataQuery;
this.props.setOrder(dataQuery, event.sort);
};
//build sort expression in dataQuery object which is sent to api calls
buildSortExpression = () => {
let expression = "";
this.props.sort.map((sort) =>
expression += `${sort.field} ${sort.dir}, `
);
expression = expression.substring(0, expression.length - 2);
const dataQuery = this.props.dataQuery;
if (expression.length > 0) {
dataQuery.sort.orderBy = expression;
}
else {
dataQuery.sort.orderBy = undefined;
}
this.props.setOrder(dataQuery, this.props.sort, true)
}
removeByKey = (myObj, deleteKey) => {
return Object.keys(myObj)
.filter(key => key !== deleteKey)
.reduce((result, current) => {
result[current] = myObj[current];
return result;
}, {});
}
render() {
return null;
}
}
Getting started with React and Redux and I have a method in my class to create new lines in a piece of text:
class JobPage extends React.Component {
componentWillMount() {
this.props.dispatch(fetchJob(this.props.match.params.slug))
}
getMultiLineText(text) {
text.split('\n').map((item, key) => {
return <span key={key}>{item}<br/></span>
})
}
render() {
const { job } = this.props;
return(
{this.getMultiLineText(job.desc)}
)
}
}
Within my render() method I try and call it using the following but the param is not passed. It's null:
{this.getMultiLineText(job.desc)}
If I just do...
{job.desc}
it prints the description.
The value is probably null during the initial render.
You can solve it for example as
{this.getMultiLineText(job.desc || '')}
You can also fix it in your reducer.