Vue calling parent component function when child component function completes - javascript

In Vue I have a main component that houses a child component which is loaded on the page with a button and the button triggers saveTaskComment(). This works perfectly, and I can get into the .finally portion of the child components function. HOwever, when it completes and gets to that point I want to make a call back to the parent component to call the method getInformation again. The way I have it right now that doesn't work so I guess $parent isn't correct in this case.
What do I need to do to get the method in teh childComponent to call the original function
mainComponent
methods: {
getInformation() {
this.$root.$emit('fetchCommentsEvent');
},
}
childComponent
saveTaskComment() {
/*function completes and gets to this step fine*/
.finally(() => {
this.$parent.getInformation();
});
}

I've made a sample on CodeSandbox to illustrate what I said in the comment.
The key point to take away here is when you insert the Child's template into your Parent's template, you want to listen to certain event and call getInformation() when the event is emitted.
<Child #foo="getInformation()">This is child.</Child>
In order to emit this foo event back to the parent, you simply do this.$emit(eventName, optionalData) from the Child component.
Since we're listening for foo event, you want to emit it like so.
this.$emit("foo");

To emit some method from a child component, you have to pass that method to the child.
MainComponent.vue:
<child-comp #getInformation="getInformation" />
ChildComponent.vue:
.finally(() => {
this.$emit('getInformation')
});
and in case you want to pass some data to the parent method, you can do that by
this.$emit('getInformation', dataVariable)

Related

React - Call setState in parent when child is called

I am building a blog in react where I get the data from a JSON-API and render it dynamically. I call setState in my app.js to get the data from JSON via axios and route with match.params to the post pages(childs). This works fine, BUT if I call the child (URL: ../blog/post1) in a seperate window it doesnt get rendered. It works if I hardcode the state.
So, I see the issue, what would be the best way to fix it?
Parent:
class App extends Component {
constructor(props) {
super();
this.state = {
posts: []
}
getPosts() {
axios.get('APIURL')
.then(res => {
let data = res.data.posts;
this.setState({
posts: data
})
})
}
componentDidMount = () => this.getPosts()
Child:
UPDATE - Found the error:
The component threw an error, because the props were empty. This was because the axios getData took some time.
Solution: I added a ternary operator to check if axios is done and displayed a css-spinner while loading. So no error in component.
Dynamic Routing works like a charme.
You can do this with binding. You would need to write a function like
setPosts(posts) {
this.setState({posts});
}
in the parent, and then in the parent, bind it in the constructor, like so.
this.setPosts = this.setPosts.bind(this);
When you do this, you're attaching setPosts to the scope of the parent, so that all mentions of this in the function refer to the parent's this instead of the child's this. Then all you need to do is pass the function down to the child in the render
<Child setPosts={this.setPosts} />
and access that method in the child with
this.props.setPosts( /*post array goes here */ );
This can apply to your getPosts method as well, binding it to the parent class and passing it down to the Child.
When you hit the /blog/post1 url, it renders only that page. The App component is not loaded. If you navigate from the App page, all the loading has been done and you have the post data in the state of the child component.
Please refer to this SO question. This should answer your question.

Handle child component update (controlled vs uncontrolled)

There is a Parent component which container a Child component.
Parent supposed to load the data (e.g. using AJAX) and update the Child inputs.
Child supposed to listen for the changes to its inputs and update them as well.
Setting Child props from Parent sets inputs values:
<input value={this.props.someVal}
But this makes it unable to self-update the child, since in order to self-update its inputs it has to use:
<input value={this.state.someVal}
Does this mean, Child has to listen for input change and triggers Parents handler-function that will sets its props?
I thought that setting components props - auto-updates corresponding state (triggering re-render) and that using <input value={this.state.someVal} should cover both cases.
There really isn't any connection between props and state. These are two different mechanisms: state is internal to the component and used for keeping encapsulated data. Props are sent by an enclosing object, as you've done.
There are several ways to go about this, but the general consensus is that having state in one place and passing it down the component tree leads to less messy code and bugs. That means your statement is correct - you'll have to pass update functions to the child component from the parent, so that when invoked it will re-render the child with new props.
I have found the way to do it. Smth like (TypeScript syntax):
componentWillReceiveProps(props: Props) {
this.setState({
data: props.data
})
}
This will allow to update the child both from the parent and child itself. Not sure whether this is a right approach, but it kinda works.
Parent component will make the ajax call and set its own state, the state values will be passed as props, which will update the child always.
class parent extends React.Component {
state = {
value: null
}
componentWillMount() {
this.makeApiCall()
}
makeApiCall = () => {
fetch("api_ulr")
.then(() => {
this.setState({
value: newValue,
})
})
}
render() {
return ( < Child value = {
this.state.value
} > < /Child>)
}
}
now child will update whenver a new data comes.

Attaching event listeners from external script file

I moved from classic HTML to React.
My classic HTML had external JavaScript, which attached even listeners upon $(document).ready(). This doesn't work anymore, as $(document).ready() is triggered before React renders the elements.
To attach the even listeners to the React page, I tried to use componentDidMount() as below, but the even listeners are not being attached.
reactJS.js:
var React = require('react');
var ReactDOM = require('react-dom');
class Question extends React.Component {
render() {
...
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {data: []};
}
loadQuestionsFromServer() {
$.ajax({
url: this.props.url,
datatype: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this)
})
}
componentWillMount() {
this.loadQuestionsFromServer();
}
componentDidMount() {
alert("component did mount");
window.attach_listeners();
}
render() {
return (
<div>
<h1>Hello React</h1>
<ul>
{this.state.data.map(function(question, i) {
return <Question key={i} question={question} />
})}
</ul>
</div>
);
}
}
ReactDOM.render(
<App url='/questions/test.json'/>,
document.getElementById('app')
);
There is a console.log() when attach_listeners(); is run and I see that componentDidMount() does trigger it.
If I manually type window.attach_listeners(); into the console it works.
Is attach_listeners() getting triggered too early maybe? When the alert() under componentDidMount() pops up, I still can't see the page. But componentDidMount() would be after render() I thought.
Any help would be greatly appreciated.
EDIT:
See below for a basic idea of attach_listeners();. This is in an external JavaScript file, which loads correctly as far as I can tell.
function attach_listeners() {
$('input.answer').keydown(function(event) {
...
});
$("a.answer").click(function() {
...
});
$("a.showanswer").click(function() {
...
});
$("input").focus(function(e) {
...
});
};
When you register for the events, the data isn't yet loaded from the server, the Question component aren't yet mounted and thus the events is not attached to anything at all.
Explanation:
In componentWillMount, you request for the data from server, this is correct and recommended, but the request is asynchronous and the state is change only when the data is returned from the server..
When componentDidMount, the data is not available, so the only elements rendered in the DOM is the h1 and ul (with no element inside).
You register for events inside the componentDidMount lifecycle method, because there's no such elements in the DOM, the callbacks are not registered.
The data is returned, the setState method called and the state updated with the new data.
The App component rerender with the new state, the Question component (together with its DOM elements input.answer,...) get rendered. Because this happen after you registered the event, the callbacks is not attached to them.
Now, when all the elements are mounted. You call the attach_listeners method and it attach the event listener to the elements correctly.
Solution:
The recommended React way: you should put the callbacks as methods of your App component, and pass them into your Question components inside the render method, like this.
Put your attach_listeners method call into componentDidUpdate. This way, your callbacks is sure to be registered, but you must be aware that if your component get rerendered again, the callbacks will be registered twice or more, as the componentDidUpdate is called after each rerender except the first.
Give your attach_listener a id param to attach to only the elements with that id. Add an id to each of your Question, and put your attach_listener call to the componentDidMount of the Question component with its id as the argument.
Your attach_listeners() is appointing to original elements, so only works when you manually type in browser console.
Event callback is bound to the react component and not the original element.
See here:
https://shripadk.github.io/react/tips/dom-event-listeners.html
https://reactjs.org/docs/handling-events.html

How to allow child component to react to a routing event before the parent component?

I am using react, react-router & redux. The structure of my app is such:
CoreLayout
-> <MaterialToolbar /> (contains back button)
-> {children} (react-router)
When the user presses the back button, which is normally handled by the CoreLayout, I would like the current child component to handle the back button instead of the parent. (In my case, I would like the current view to check if its data has been modified, and pop up an 'Are you sure you wish to cancel?' box before actually going back.) If the child does not wish to handle this, the parent will do it's thing.
Another example would be allowing a childview to set the title in the toolbar.
My reading has told me that accessing a component through a ref and calling a method on it is not the react way -- this is also made a bit more difficult since I am using redux-connect. What is the correct way to implement this behavior?
This is how I would do it, assuming you mean your navigation back button (and not the browser back button):
class CoreLayout extends Component {
handleBack () {
//... use router to go back
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, child => React.cloneElement(child, { onBack: this.handleBack }))}
</div>
}
}
class Child extends Component {
handleBackButtonClick () {
// Here perform the logic to decide what to do
if (dataHasBeenModifiedAndConfirmed) {
// Yes, user wants to go back, call function passed by the parent
this.props.onBack()
} else {
// User didn't confirm, decide what to do
}
}
render () {
return <div onClick={this.handleBackButtonClick.bind(this)}>
Go Back
</div>
}
}
You simply pass a function from the parent to the child via props. Then in the child you can implement the logic to check if you really want to delegate the work to the parent component.
Since you use react-router and your children are passed to your parent component through this.props.children, to pass the onBack function you need to map the children and use React.cloneElement to pass your props (see this answer if you need more details on that: React.cloneElement: pass new children or copy props.children?).
Edit:
Since it seems you want to let the children decide, you can do it this way (using refs):
class CoreLayout extends Component {
constructor () {
super()
this.childRefs = {};
}
handleBack () {
for (let refKey in Object.keys(this.childRefs) {
const refCmp = this.childRefs[refKey];
// You can also pass extra args to refCmp.shouldGoBack if you need to
if (typeof refCmp.shouldGoBack === 'function' && !refCmp.shouldGoBack()) {
return false;
}
}
// No child requested to handle the back button, continue here...
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, (child, n) => React.cloneElement(child, {
ref: cmp => { this.childRefs[n] = cmp; }
}))}
</div>
}
}
class Child extends Component {
shouldGoBack () {
// Return true/false if you do/don't want to actually go back
return true
}
render () {
return <div>
Some content here
</div>
}
}
This is a bit more convoluted as normally with React it's easier/more idiomatic to have a "smart" parent that decides based on the state, but given your specific case (back button in the parent and the logic in the children) and without reimplementing a few other things, I think using refs this way is fine.
Alternatively (with Redux) as the other answer suggested, you would need to set something in the Redux state from the children that you can use in the parent to decide what to do.
Hope it's helpful.
I don't think there is a correct way to solve this problem, but there are many ways. If I understand your problem correctly, most of the time the back button onClick handler will be handled within CoreLayout, but when a particular child is rendered that child will handle the onClick event. This is an interesting problem, because the ability to change the functionality of the back button needs to be globally available, or at very least available in CoreLayout and the particular child component.
I have not used redux, but I have used Fluxible and am familar with the Flux architecture and the pub/sub pattern.
Perhaps you can utilize your redux store to determine the functionality of your back button. And your CoreLayout component would handle rendering the prompt. There is a bug with the following code, but I thought I would not delete my answer for the sake of giving you an idea of what I am talking about and hopefully the following code does that. You would need to think through the logic to get this working correctly, but the idea is there. Use the store to determine what the back button will do.
//Core Layout
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
// backFunction is a string correlating to name of function in Core Layout Component
if(state.backFunction) {
// lets assume backFunction is 'showModal'. Execute this.showModal()
// and let it handle the rest.
this[state.backFunction]();
// set function to false so its not called everytime the store updates.
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: false})
}
})
}
showModal() {
// update state, show modal in Core Layout
if(userWantsToGoBack) {
this.onBack();
// update store backFunction to be the default onBack
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'onBack'})
// if they don't want to go back, hide the modal
} else {
// hide modal
}
}
onBack() {
// handle going back when modal doesn't need to be shown
}
The next step is to update your store when the child component mounts
// Child component
componentDidMount(){
// update backFunction so when back button is clicked the appropriate function will be called from CoreLayout
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'showModal'});
}
This way you don't need to worry about passing any function to your child component you let the state of the store determine which function CoreLayout will call.

ReactJS: How to make a component call a method only the very first it is rendered?

I would like for a component to call a method only once, and only the very first time the component gets rendered. I attempted it in constructor(), thinking that it is supposed to occur only once and only when it is first mounted ever, but looks like whenever that component is rendered again, the constructor() is called again as well.
Is there a way to have a component call a method only once and only the very first time it is rendered?
Thank you
componentWillMount() gets called pre-render and only once until the page refreshes.
https://facebook.github.io/react/docs/react-component.html#componentwillmount
componentDidMount() gets called immediately after render() and the DOM is available at this time. This will happen only the first time it's loaded until the page refreshes.
https://facebook.github.io/react/docs/react-component.html#componentdidmount
you can use getDerivedStateFromProps and pass an empty parameter while you navigate, that triggers the method once after the navigation.
// caller
this.props.navigation.navigate('SOMEWHERE', {})
// SOMEWHERE
static getDerivedStateFromProps(nextProps, prevState){
doSomething()
return null
}
You could set a variable in localStorage when you first load your page. Then, whenever you render, you simply get and test that variable:
async componentDidMount() {
...
if(localStorage.getItem('reRender') !== "true") {
//Processing to do only for the very first render
localStorage.setItem('reRender', "true");
}
}
You could reset the variable depending on your use case afterwards (for example when logging out):
localStorage.removeItem('reRender');

Categories

Resources