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.
Related
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.
I have two components - a sign in form component that holds the form and handles login logic, and a progress bar similar to the one on top here in SO. I want to be able to show my progress bar fill up as the login logic executes if that makes sense, so as something is happening show the user an indication of loading. I've got the styling sorted I just need to understand how to correctly trigger the functions.
I'm new to React so my first thought was to define handleFillerStateMax() and handleFillerStateMin() within my ProgressBarComponent to perform the state changes. As the state changes it basically changes the width of the progress bar, it all works fine. But how do I call the functions from ProgressBarComponent as my Login component onSubmit logic executes? I've commented my ideas but they obviously don't work..
ProgressBarComponent:
class ProgressBarComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
percentage: 0
}
}
// the functions to change state
handleFillerStateMax = () => {
this.setState ({percentage: 100})
}
handleFillerStateMin = () => {
this.setState ({percentage: 0})
}
render () {
return (
<div>
<ProgressBar percentage={this.state.percentage}/>
</div>
)
}
}
Login component:
class SignInFormBase extends Component {
constructor(props) {
super(props);
this.state = {...INITIAL_STATE};
}
onSubmit = event => {
const {email, password} = this.state;
// ProgressBarComponent.handleFillerMax()????
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
//ProgressBarComponent.handleFillerMin()????
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}
Rephrase what you're doing. Not "setting the progress bar's progress" but "modifying the applications state such that the progress bar will re-render with new data".
Keep the current progress in the state of the parent of SignInFormBase and ProgressBarComponent, and pass it to ProgressBarComponent as a prop so it just renders what it is told. Unless there is some internal logic omitted from ProgressBar that handles its own progress update; is there?
Pass in a callback to SignInFormBase that it can call when it has new information to report: that is, replace ProgressBarComponent.handleFillerMax() with this.props.reportProgress(100) or some such thing. The callback should setState({progress: value}).
Now, when the SignInFormBase calls the reportProgress callback, it sets the state in the parent components. This state is passed in to ProgressBarComponent as a prop, so the fact that it changed will cause he progress bar to re-render.
Requested example for #2, something like the following untested code:
class App extends Component {
handleProgressUpdate(progress) {
this.setState({progress: progress});
}
render() {
return (
<MyRootElement>
<ProgressBar progress={this.state.progress} />
<LoginForm onProgressUpudate={(progress) => this.handleProgressUpdate(progress)} />
</MyRootElemen>
)
}
}
The simply call this.props.onProgressUpdate(value) from LoginForm whenever it has new information that should change the value.
In basic terms, this is the sort of structure to go for (using useState for brevity but it could of course be a class-based stateful component if you prefer):
const App = ()=> {
const [isLoggingIn, setIsLoggingIn] = useState(false)
const handleOnLoginStart = () => {
setIsLoggingIn(true)
}
const handleOnLoginSuccess = () => {
setIsLoggingIn(false)
}
<div>
<ProgressBar percentage={isLoggingIn?0:100}/>
<LoginForm onLoginStart={handleOnLogin} onLoginSuccess={handleOnLoginSuccess}/>
</div>
}
In your LoginForm you would have:
onSubmit = event => {
const {email, password} = this.state;
this.props.onLoginStart() // <-- call the callback
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
this.props.onLoginSuccess() // <-- call the callback
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}
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);
I was reading the section on Don’t Mutate the Original Component. Use Composition from this link.
https://reactjs.org/docs/higher-order-components.html
I then reviewed a project I'm trying to build. At a high level, this is what my code looks like:
class Wrapper extends Component {
constructor(props) {
this.wrappedComponent = props.wrappedComponent;
}
async componentWillAppear(cb) {
await this.wrappedComponent.prototype.fetchAllData();
/* use Greensock library to do some really fancy animation on the wrapper <Animated.div> */
this.wrappedComponent.prototype.animateContent();
cb();
}
render() {
<Animated.div>
<this.wrappedComponent {...this.props} />
</Animated.div>
}
}
class Home extends Component {
async fetchAllData(){
const [r1,r2] = await Promise.All([
fetch('http://project-api.com/endpoint1'),
fetch('http://project-api.com/endpoint2')
]);
this.setState({r1,r2});
}
animateContent(){
/* Use the GreenSock library to do fancy animation in the contents of <div id="result"> */
}
render() {
if(!this.state)
return <div>Loading...</div>;
return (
<div id="result">
{this.state.r1.contentHTML}
</div>
);
}
}
export default class App extends Component {
render() {
return <Wrapper wrappedComponent={Home} />;
}
}
My questions are:
In my Wrapper.componentWillAppear(), I fire the object methods like this.wrappedComponent.prototype.<methodname>. These object methods can set it's own state or animate the contents of the html in the render function. Is this considered mutating the original component?
If the answer to question 1 is yes, then perhaps I need a better design pattern/approach to do what I'm trying to describe in my code. Which is basically a majority of my components need to fetch their own data (Home.fetchAllData(){then set the state()}), update the view (Home.render()), run some generic animation functions (Wrapper.componentWillAppear(){this.animateFunctionOfSomeKind()}), then run animations specific to itself (Home.animateContent()). So maybe inheritance with abstract methods is better for what I want to do?
I would probably actually write an actual Higher Order Component. Rather than just a component which takes a prop which is a component (which is what you have done in your example). Predominately because I think the way you have implemented it is a bit of a code smell / antipattern.
Something like this, perhaps.
class MyComponent extends React.Component {
constructor() {
super();
this.animateContent = this.animateContent.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.props.r1 !== nextProps.r1) {
this.animateContent();
}
}
componentDidMount() {
// do your fetching and state setting here
}
animateContent() {
// do something
}
render() {
if(!this.props.r1) {
return <div>Loading...</div>;
}
return (
<div id="result">
{this.props.r1.title}
</div>
);
}
}
const myHOC = asyncFn => WrappedComponent => {
return class EnhancedComponent extends React.Component {
async componentDidMount(){
const [r1, r2] = await asyncFn();
this.setState({ r1, r2 })
this.animateContent();
}
animateContent = () => {
// do some animating for the wrapper.
}
render() {
return (<WrappedComponent {...this.props} {...this.state} />)
}
}
}
const anAsyncExample = async () => {
const result = await fetch("https://jsonplaceholder.typicode.com/posts");
return await result.json();
}
const MyEnhancedComponent = myHOC(anAsyncExample)(MyComponent);
Here's a working JSFiddle so you can see it in use:
https://jsfiddle.net/patrickgordon/69z2wepo/96520/
Essentially what I've done here is created a HOC (just a function) which takes an async function and returns another function which takes and a component to wrap. It will call the function and assign the first and second result to state and then pass that as props to the wrapped component. It follows principles from this article: https://medium.com/#franleplant/react-higher-order-components-in-depth-cf9032ee6c3e
I'm trying to use props inside an event handler. This is a part of my code
class Dashboard extends Component {
componentDidMount() {
var grid = new Muuri('.grid', {
//options...
});
grid.on('move', (data) => {
console.log('ok')
//can't use this.props here
);
}
render() {...}
constructor() {...}
}
The problem is I am not able to access this.props inside the 'move' handler.
You can store the reference for this.props and refer it inside the event handler.
Or
Access the required individual properties using destructors and then access those properties inside event handler.
class Dashboard extends Component {
componentDidMount() {
const {prop1name, prop2Name} = this.props;
//OR
const thisProps = this.props;
var grid = new Muuri('.grid', {
//options...
});
grid.on('move', (data) => {
console.log('ok')
//access `this.props` using `thisProps` or access individual properties.
)}
render() {}
constructor() {}
}