I'm trying to use react to create a list of elements, and update the state of the parent of this list when a single element is clicked.
The overall container is App.jsx (the grandparent)
class App extends Component {
constructor(props) {
super(props);
this.state = {
selectedClass: null,
query: "cs 2110"
}
this.handleSelectClass.bind(this);
}
handleSelectClass(classId) {
console.log(classId);
//get the id of the course and get full course details
Meteor.call('getCourseById', classId, function(error, result) {
if (!error) {
console.log(this.state);
this.setState({selectedClass: result, query: ""}, function() {
console.log(this.state.selectedClass);
console.log(this.state.query);
});
} else {
console.log(error)
}
});
}
//check if a class is selected, and show a coursecard only when one is.
renderCourseCard() {
var toShow = <div />; //empty div
if (this.state.selectedClass != null) {
toShow = <CourseCard course={this.state.selectedClass}/>;
}
return toShow;
}
render() {
return (
<div className="container">
<header>
<h1>Todo List</h1>
</header>
<div className='row'>
<input />
<Results query={this.state.query} clickFunc={this.handleSelectClass}/>
</div>
<div className='row'>
<div className="col-md-6">
{this.renderCourseCard()}
</div>
<div className="col-md-6 panel-container fix-contain">
<Form courseId="jglf" />
</div>
</div>
</div>
);
}
}
The parent container is Results.jsx
export class Results extends Component {
constructor(props) {
super(props);
}
renderCourses() {
if (this.props.query != "") {
return this.props.allCourses.map((course) => (
//create a new class "button" that will set the selected class to this class when it is clicked.
<Course key={course._id} info={course} handler={this.props.clickFunc}/>
));
} else {
return <div />;
}
}
render() {
return (
<ul>
{this.renderCourses()}
</ul>
);
}
}
and the Course list item is a grandchild component
export default class Course extends Component {
render() {
var classId = this.props.info._id;
return (
<li id={classId} onClick={() => this.props.handler(classId)}>{this.props.info.classFull}</li>
);
}
}
I followed the suggestions here Reactjs - How to pass values from child component to grand-parent component? to pass down a callback function, but the callback still does not recognize the state of the grandparent. console.log(this.state) in App.jsx returns undefined even though the classId is correct, and the error says "Exception in delivering result of invoking 'getCourseById': TypeError: this.setState is not a function"
Is this a problem with the binding? I've tried this without Course as its own component and have the same issue.
Quickly looking through the code. I can see that problem one lies here. Even though you've bounded your function to your component, you're using a meteor call that scopes the result in it's own function scope which means that it won't be able to access this.setState. You can use fat arrow function to get around this problem, but you need to make sure that you are using ES6.
Meteor.call('getCourseById', classId, function(error, result) => {
if (!error) {
console.log(this.state);
this.setState({selectedClass: result, query: ""}, function() {
console.log(this.state.selectedClass);
console.log(this.state.query);
});
} else {
console.log(error)
}
});
TO
Meteor.call('getCourseById', classId, (error, result) => {
if (!error) {
console.log(this.state);
this.setState({selectedClass: result, query: ""}, () => {
console.log(this.state.selectedClass);
console.log(this.state.query);
});
} else {
console.log(error)
}
});
You've also binded your function incorrectly to your component.
this.handleClassSubmit = this.handleClassSubmit.bind(this);
Related
I made a search component, I am passing the results of the search back to the parent using props. The issue is it will not setState until the function is triggered so I get the error of undefined in the map loop.
I am trying to show all results until the search is triggered using onChange.
How can I accomplish this.
//Search Component
export default class Searchbar extends Component {
constructor(props){
super(props)
}
state = {
input : '',
visable:[],
}
onChange=(e)=>{
this.setState({input: e.target.value})
let clone = [...this.props.theGitUsers]
if(e.value === ''){
this.setState({visable:clone})
}else{
let filteredSearch = clone.filter((loginUsers)=>{
return loginUsers.login.toUpperCase().includes(this.state.input.toUpperCase())
})
this.setState({visable:filteredSearch})
}
//Passing the state results to App Component using this props to the App function function
this.props.searchRes(this.state.visable);
}
render() {
return (
<div>
<input type="text" onChange= {this.onChange} value={this.state.input} />
</div>
)
}
}
//App.js Parent ////////////////////////
state={
gitusers:[],
searched:[],
loading:false,
}
componentDidMount(){
this.setState({loading:true});
setTimeout(() => {
axios.get('https://api.github.com/users')
.then(res=> this.setState({gitusers:res.data , loading:false}))
}, 1000);
}
//The search results from Searchbar Component
searchRes=(visable)=>{
this.setState({searched:visable})
}
render(){
return (
<div>
<Navbar title="Github Finder" icons="fab fa-github" />
<Searchbar theGitUsers = {this.state.gitusers} searchRes = {this.searchRes} />
<div className = "container">
<TheUsers gituser = {this.state.gitusers} loading={this.state.loading} />
</div>
</div>
);
}
Always check for empty or null while running map over array.
userSearch && userSearch.map(item => {
console.log(item);
});
I'm currently following this and I did get it to work. But I would like to know if there is a way to stop the Query Render from reloading the data when calling this.setState(). Basically what I want is when I type into the textbox, I don't want to reload the data just yet but due to rendering issues, I need to set the state. I want the data to be reloaded ONLY when a button is clicked but the data will be based on the textbox value.
What I tried is separating the textbox value state from the actual variable passed to graphql, but it seems that regardless of variable change the Query will reload.
Here is the code FYR.
const query = graphql`
query TestComponentQuery($accountId: Int) {
viewer {
userWithAccount(accountId: $accountId) {
name
}
}
}
`;
class TestComponent extends React.Component{
constructor(props){
super(props);
this.state = {
accountId:14,
textboxValue: 14
}
}
onChange (event){
this.setState({textboxValue:event.target.value})
}
render () {
return (
<div>
<input type="text" onChange={this.onChange.bind(this)}/>
<QueryRenderer
environment={environment}
query={query}
variables={{
accountId: this.state.accountId,
}}
render={({ error, props }) => {
if (error) {
return (
<center>Error</center>
);
} else if (props) {
const { userWithAccount } = props.viewer;
console.log(userWithAccount)
return (
<ul>
{
userWithAccount.map(({name}) => (<li>{name}</li>))
}
</ul>
);
}
return (
<div>Loading</div>
);
}}
/>
</div>
);
}
}
Okay so my last answer didn't work as intended, so I thought I would create an entirely new example to demonstrate what I am talking about. Simply, the goal here is to have a child component within a parent component that only re-renders when it receives NEW props. Note, I have made use of the component lifecycle method shouldComponentUpdate() to prevent the Child component from re-rendering unless there is a change to the prop. Hope this helps with your problem.
class Child extends React.Component {
shouldComponentUpdate(nextProps) {
if (nextProps.id === this.props.id) {
return false
} else {
return true
}
}
componentDidUpdate() {
console.log("Child component updated")
}
render() {
return (
<div>
{`Current child ID prop: ${this.props.id}`}
</div>
)
}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
id: 14,
text: 15
}
}
onChange = (event) => {
this.setState({ text: event.target.value })
}
onClick = () => {
this.setState({ id: this.state.text })
}
render() {
return (
<div>
<input type='text' onChange={this.onChange} />
<button onClick={this.onClick}>Change ID</button>
<Child id={this.state.id} />
</div>
)
}
}
function App() {
return (
<div className="App">
<Parent />
</div>
);
}
Expecting effect: click <li> --> take index --> send this index to component Watch.
When I click <li>, I grab the index and move it to theWatch component. However, when I click the second li it returns the index of the one I clicked for the first time. I think this is because it updates this index via componentDidMount. How can I reference this index after componentDidMount?
Todo
class Todo extends Component {
render () {
return (
<div className = "itemTodos" onClick={()=> this.props.selectTodo(this.props.index)}>
</div>
)
}
}
export default Todo;
App
class App extends Component {
constructor(){
super();
this.state {
selectedTodoIndex: index
}
}
selectTodo = (index) => {
this.setState({
selectedTodoIndex: index
})
}
render () {
return (
<div>
<ul>
{
this.state.todos
.map((todo, index) =>
<Todo
key={index}
index={index}
todo={todo}
selectTodo ={this.selectTodo}
/>
)
}
</ul>
<Watch
selectedTodoIndex = {selectedTodoIndex}
/>
</div>
)
}
}
export default App;
Watch
class Watch extends Component {
constructor(){
super();
this.state = {
selectIndex: null
}
}
componentDidMount() {
this.setState({
selectIndex: this.props.selectedTodo
});
}
render () {
return (
<div>
</div>
)
}
}
First of all you you use selectedTodoIndex in
<Watch
selectedTodoIndex = {selectedTodoIndex}
/>
but it not specified in your render code. Add
const {selectedTodoIndex} = this.state;
in render function.
Second, use componentDidUpdate in Watch for update inner state on props update:
class Watch extends Component {
constructor(){
super();
this.state = {
selectIndex: null
}
}
componentDidMount() {
this.setState({
selectIndex: this.props.selectedTodo
});
}
componentDidUpdate (prevProps) {
if (prevProps.selectedTodo !== this.props.selectedTodo)
this.setState({
selectIndex: this.props.selectedTodo
});
}
render () {
return (
<div>
</div>
)
}
}
If i am not wrong your Todo component is in watch??. So Watch component should be like this :
render () {
return (
<div>
<Todo index={this.state.selectedIndex} selectedTodo={this.props.selectedTodoIndex}/>
</div>
)
}
Here i made codesandbox of this code . Feel free to checkout and let me know if you any doubt. Code link : https://codesandbox.io/s/frosty-chaplygin-ws1zz
There are lot of improvements to be made. But I believe what you are looking for is getDerivedStateFromProps lifeCycle method in Watch Component. So the code will be:
getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.selectedTodoIndex !== prevState.selectedTodoIndex) {
return { selectIndex: nextProps.selectedTodoIndex }
}
}
This will check if the selected index has changed in App Component, if yes it will update the state in Watch Component.
What I have
I have a class that's not being exported but being used internally by a file. (The classes are all in the same file)
class SearchResults extends Component {
constructor()
{
super();
this.fill_name = this.fill_name.bind(this);
}
fill_name(event, name)
{
console.log("search results", event.target);
this.props.fill_name(name, event.target);
}
render()
{
return (
<div className="search-results-item" onClick={ () => this.fill_name(event, this.props.name)}>
<div className="highlight">
{this.props.name}
</div>
</div>
)
}
}
I'm trying to get the <div> element to be sent back to the parent, which is defined below (skipping irrelevant stuff):
class PublishComponent extends Component {
fill_name(name, me)
{
console.log(me);
$("#company_input").val(name);
this.setState({ list: { ...this.state.list, company: [] } });
}
}
me is the event.
What I'm getting
The console posts the following:
search results <react></react>
undefined
so the event.target is <react></react> for some reason, while the me is getting undefined.
Expected behaviour
It should return the element i.e. <div className="search-results-item"...></div>
You are not passing the event object
Change This
<div
className="search-results-item"
onClick={() => this.fill_name(event, this.props.name)}
/>
To This
<div
className="search-results-item"
// pass the event here
onClick={event => this.fill_name(event, this.props.name)}
/>
This should work for you:
class SearchResults extends Component {
constructor() {
super();
this.fill_name = this.fill_name.bind(this);
}
fill_name() {
this.props.fill_name(this.props.name, this.ref)
}
render() {
return (
<div className="search-results-item" ref={ref => { this.ref = ref }} onClick={this.fill_name}>
<div className="highlight">
{this.props.name}
</div>
</div>
)
}
}
First watch this, so you can see the behavior going on.
Timing Issue (JS in one component relies on another component to exist first)
I need to be able to somehow check that another component exists before I apply this JS in this component's ComponentDidMount
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
const el = ReactDOM.findDOMNode(this);
console.log("table of contents mounted");
if(document.getElementById('interview-heading') && el) {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
}
},
it does hit my if statement and does hit the Sticky() function but I still think I have problems when I refresh the page whereas this JS isn't working on the interview-heading component for some reason.
Note the id="interview-heading" below.
const InterviewContent = Component({
componentDidMount() {
console.log("InterviewContent mounted");
},
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
const InterviewContentMain = Component({
componentDidMount() {
console.log("InterviewContentMain mounted");
},
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents company={company}/>
</div>
<div className="all-80">
<InterviewContainer company={company}/>
</div>
</div>
</div>
)
}
})
export default InterviewContent;
I realize TableOfContents is being rendered before InterviewContent because it's a child of TableOfContents and I believe in React children are rendered before their parents (inside-out)?
I think you need to rethink your component structure. I don't know your entire setup, but it looks like you should probably have a shared parent component pass the message from TableOfContents to InterviewContent:
const InterviewContentMain = Component({
getInitialState() {
return {
inkEnabled: false
}
},
componentDidMount() {
console.log("InterviewContentMain mounted");
},
enableInk() {
this.setState({ inkEnabled: true });
}
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents inkEnabled={this.state.inkEnabled} company={company}/>
</div>
<div className="all-80">
<InterviewContainer enableInk={this.enableInk} company={company}/>
</div>
</div>
</div>
)
}
})
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
console.log("table of contents mounted");
this.props.enableInk();
},
...
const InterviewContent = Component({
enableInk() {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
},
// willReceiveProps isn't called on first mount, inkEnabled could be true so
componentDidMount() {
if (this.props.inkEnabled) {
this.enableInk();
}
},
componentWillReceiveProps(nextProps) {
if (this.props.inkEnabled === false && nextProps.inkEnabled === true) {
this.enableInk();
}
}
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
Then have componentDidMount trigger this.props.enableInk().
Or better yet, why not just put the Ink.UI.Sticky call in componentDidMount of InterviewContent?