Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to extend state in SPA? #81

Closed
jkleiser opened this issue Nov 26, 2019 · 14 comments
Closed

How to extend state in SPA? #81

jkleiser opened this issue Nov 26, 2019 · 14 comments

Comments

@jkleiser
Copy link

I have just created the basic SPA by doing npx apprun --init --spa. Now I want the app's state to take care of more than just the component name (Home, About, Contact). I may e.g. want to have a counter (+1/-1) on one of the components. In the basic SPA one gets the impression that the component name is the app's state (state = 'Home';), but right now it seems that it's not necessarily so. I'm a bit confused.

@jkleiser
Copy link
Author

I now see that the way to extend the state, is e.g. like this:

state = { page: 'About', count: 0 };

and add buttons like this in the view function:

<button type="button" onclick={() => this.run("+1")}>+1</button>

and event handlers like this in the update object:

'+1': state => {
  return { ...state, count: state.count + 1 };
}

What confused me initially, was the meaning and function of the original state = 'About'. It looked as if it was modifying a global state, while it is only initialising the AboutComponent's state. I'm still not sure why it we don't have to refer to this state with this.state.

@jkleiser
Copy link
Author

And as explained here https://medium.com/@yiyisun/announcing-apprun-directives-6a063f88379c, the button can be simplified like this:

<button type="button" $onclick="+1">+1</button>

@yysun
Copy link
Owner

yysun commented Nov 27, 2019 via email

@jkleiser
Copy link
Author

Thanks for your answer. I obviously should have studied your docs on components better. In that section (05-component#class-fields), however, I have problem understanding that the events generated by use of app.run in the button onclick will be picked up by the +1/-1 event handlers in that same Counter example. In my AboutComponent, which has pretty much the same structure as that Counter, I had to use this.run (or the $onclick style) for the events to be picked up.

@yysun
Copy link
Owner

yysun commented Nov 27, 2019 via email

@jkleiser
Copy link
Author

If I change this.run("+1") to app.run("+1") in the onclick of the +1 button in my AboutComponent, I get "app.ts:33 Assertion failed: No subscriber for event: +1" when I click that button. I conclude that app.run requires a global event handler.

@yysun
Copy link
Owner

yysun commented Nov 28, 2019

You’re right. By default, component events are local. You can prefix the event name with #, / or @ to make it global.

class Counter extends Component {
   update = {
      '+1': state=>state+1, // local event
      '#+1': state=>state+1, // global event
   }
}

@jkleiser
Copy link
Author

Can you explain the reason for having <span class="sr-only">(current)</span> in the Home nav-link element in the main.tsx file?

@yysun
Copy link
Owner

yysun commented Nov 29, 2019 via email

@jkleiser
Copy link
Author

jkleiser commented Dec 1, 2019

I need to fetch some global state when my SPA starts. I guess this global state could be initialised in main.tsx, probably in the app.on handler. I need this global state to be accessible from the individual components of my SPA. Would I need to pass this global state to the app.render call in main.tsx? This is typical when using app.start. Where can I find descriptions of the app.render and app.start calls?

@jkleiser
Copy link
Author

jkleiser commented Dec 2, 2019

Initialising global state in the app.on handler in main.tsx now doesn't look like a good idea, as that handler is being called every time one switch view from one component to another. Would it be a better idea to have "global data" in one component, and then let other components have read access to that data? I could give it a try.
I have tried to put 'mounted' functions, like the one below, into a couple of the components of my SPA, but for some reason they don't seems to get called. What may be the reason for that?

  mounted = (props: any, children: any[], state) => {
    console.log("MyComp.mounted");
    return state;
  }

@yysun
Copy link
Owner

yysun commented Dec 3, 2019

I would suggest creating a module, e.g. global.ts

export let global_state = {
 name: 'xxx'
}

And import it in main.tsx and other components

import { global_state } from './global';

You can also export a function for updating the global state in global.ts

export let global_state = {
  name: 'xxx'
}
export const set_global_state = state => {
  global_state = state;
}

To call the function, import it:

import { set_global_state } from './global';
set_global_state({ ... })

If you want to notify other components, send a global event in the set_global_state function:

import app from "apprun";
export let global_state = {
  name: 'xxx'
}
export const set_global_state = state => {
  global_state = state;
  app.run('@state_updated', state);
}

Other components can subscribe to the events and merge the global state into the local state:

import { app, Component } from "apprun";
class MyComponent extends Component {
  state = {}
  view = state => <div>{state}</div>
  update = {
    '@state_updated': (state, global_state) => {
      return { ...state, global_state}
    }
  }
}

@yysun
Copy link
Owner

yysun commented Dec 3, 2019

It is unclear in the documentation that the mounted lifecycle function is only called in the statefull child component.

class Child extends Component {
  state = {} // you can define the initial state
  view = state => <div></div>
  update = {}
  mounted = (props, children) => { ...state, ...props } // this will be called, you can merge props into the state
}

class Parent extends Component {
  state = {} // you can define the initial state
  view = state => <div>
    <Child />
  </div>
  update = {}
  mounted = () => { } // this will NOT be called when component is created using the constructor
}
new Parent().start(document.body); 

However, you can pass the initial state in to the component's constructor directly:

const init_state = {};
new Parent(init_state).start(document.body); 

@jkleiser
Copy link
Author

jkleiser commented Dec 3, 2019

Thanks a lot for the module suggestion. It looks very promising.

@yysun yysun closed this as completed Feb 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants