I'm super new to React and I have two components I want to toggle between based on a user click. I've went about it by creating a 'currentView' state and using that to set/update what the user should be looking at, but when I try to get the components to display it throws tag errors. Here's my code:
class App extends Component {
constructor(props){
super(props);
this.state={
currentView: "Welcome",
};
}
goTrack() {
this.setState({currentView: "Tracker"});
console.log(this.state.currentView);
}
goReview() {
this.setState({currentView: "Review"});
console.log(this.state.currentView);
}
render() {
return (
<div className="App">
<aside>
<nav>
<ul>
<li onClick={()=>this.goTrack()}> Tracker </li>
<li onClick={()=>this.goReview()}> Review </li>
</ul>
</nav>
</aside>
<main>
<this.state.currentView/>
</main>
</div>
);
}
}
export default App;
My question is how should I go about dynamically selecting components to display without re-rendering the entire DOM?
One way to solve this is to use the current state to match a key in an object containing the component you want in a certain state.
constructor() {
super()
this.state = {
current: 'welcome',
}
}
render() {
const myComponents = {
welcome: <Welcome />,
tracker: <Tracker />,
}
const CurrentComponent = myComponents[this.state.current]
return (
<CurrentComponent />
)
}
And when you change the state current with the value 'tracker', Tracker component will be rendered instead of Welcome component.
I am guessing Tracker and Review are two components you want to toggle based on value in this.state.currentView
In short this <this.state.currentView/> expects a component/html element.
one way to do what you want to do would be to do this instead.
<main>
{ this.state.currentView == 'Review' && (
<Review />
)}
{ this.state.currentView == 'Tracker' && (
<Tracker />
)}
</main>
Option 1: Conditions
render() {
const { currentView } = this.state;
// I omit the other stuff to focus on your question
return (
<div>
{currentView === 'Welcome' && <Welcome />}
{currentView === 'Tracker' && <Tracker />}
{currentView === 'Review' && <Review />}
</div>
);
}
Option 2: Dynamic component
import Welcome from './Welcome';
import Review from './Review';
class App extends Component {
constructor(props) {
super(props);
this.state = {
current: Welcome,
};
}
// ... stuff ...
goTrack() {
this.setState(prevState => { ...prevState, current: Review });
}
// ... other stuff ...
render() {
# I rename it because React expects a component's name
# with a capital name.
const { current: Current } = this.state;
# As above, I put only relevant rendering
return <div><Current /></div>;
}
}
Option 3: Guess
And actually, looking at what you are trying to do, I'd suggest you have a look at react-router-dom.
Related
I'm super new to react, this is probably a terrible question but I'm unable to google the answer correctly.
I have a component (CogSelector) that renders the following
import React from "react"
import PropTypes from "prop-types"
import Collapsible from 'react-collapsible'
import Cog from './cog.js'
const autoBind = require("auto-bind")
import isResultOk from "./is-result-ok.js"
class CogSelector extends React.Component {
constructor(props) {
super(props)
this.state = {
docs: null,
loaded: false,
error: null
}
autoBind(this)
}
static get propTypes() {
return {
selectCog: PropTypes.func
}
}
shouldComponentUpdate(nextProps, nextState){
if (nextState.loaded === this.state.loaded){
return false;
} else {
return true;
}
}
componentDidMount() {
fetch("/api/docs")
.then(isResultOk)
.then(res => res.json())
.then(res => {
this.setState({docs: res.docs, loaded: true})
}, error => {
this.setState({loaded: true, error: JSON.parse(error.message)})
})
}
render() {
const { docs, loaded, error } = this.state
const { selectCog } = this.props
if(!loaded) {
return (
<div>Loading. Please wait...</div>
)
}
if(error) {
console.log(error)
return (
<div>Something broke</div>
)
}
return (
<>
Cogs:
<ul>
{docs.map((cog,index) => {
return (
<li key={index}>
<Cog name={cog.name} documentation={cog.documentation} commands={cog.commands} selectDoc={selectCog} onTriggerOpening={() => selectCog(cog)}></Cog>
</li>
// <li><Collapsible onTriggerOpening={() => selectCog(cog)} onTriggerClosing={() => selectCog(null)} trigger={cog.name}>
// {cog.documentation}
// </Collapsible>
// </li>
)
})}
{/* {docs.map((cog, index) => { */}
{/* return ( */}
{/* <li key={index}><a onClick={() => selectCog(cog)}>{cog.name}</a></li>
)
// })} */}
</ul>
</>
)
}
}
export default CogSelector
the collapsible begins to open on clicking, then it calls the selectCog function which tells it's parent that a cog has been selected, which causes the parent to rerender which causes the following code to run
class DocumentDisplayer extends React.Component{
constructor(props) {
super(props)
this.state = {
cog: null
}
autoBind(this)
}
selectCog(cog) {
this.setState({cog})
}
render(){
const { cog } = this.state
const cogSelector = (
<CogSelector selectCog={this.selectCog}/>
)
if(!cog) {
return cogSelector
}
return (
<>
<div>
{cogSelector}
</div>
<div>
{cog.name} Documentation
</div>
<div
dangerouslySetInnerHTML={{__html: cog.documentation}}>
</div>
</>
)
}
}
export default DocumentDisplayer
hence the cogSelector is rerendered, and it is no longer collapsed. I can then click it again, and it properly opens because selectCog doesn't cause a rerender.
I'm pretty sure this is just some horrible design flaw, but I would like my parent component to rerender without having to rerender the cogSelector. especially because they don't take any state from the parent. Can someone point me to a tutorial or documentation that explains this type of thing?
Assuming that Collapsible is a stateful component that is open by default I guess that the problem is that you use your component as a variable instead of converting it into an actual component ({cogSelector} instead of <CogSelector />).
The problem with this approach is that it inevitably leads to Collapsible 's inner state loss because React has absolutely no way to know that cogSelector from the previous render is the same as cogSelector of the current render (actually React is unaware of cogSelector variable existence, and if this variable is re-declared on each render, React sees its output as a bunch of brand new components on each render).
Solution: convert cogSelector to a proper separated component & use it as <CogSelector />.
I've recently published an article that goes into details of this topic.
UPD:
After you expanded code snippets I noticed that another problem is coming from the fact that you use cogSelector 2 times in your code which yields 2 independent CogSelector components. Each of these 2 is reset when parent state is updated.
I believe, the best thing you can do (and what you implicitly try to do) is to lift the state up and let the parent component have full control over all aspects of the state.
I solved this using contexts. Not sure if this is good practice but it certainly worked
render() {
return (
<DocContext.Provider value={this.state}>{
<>
<div>
<CogSelector />
</div>
{/*here is where we consume the doc which is set by other consumers using updateDoc */}
<DocContext.Consumer>{({ doc }) => (
<>
<div>
Documentation for {doc.name}
</div>
<pre>
{doc.documentation}
</pre>
</>
)}
</DocContext.Consumer>
</>
}
</DocContext.Provider>
)
}
then inside the CogSelector you have something like this
render() {
const { name, commands } = this.props
const cog = this.props
return (
//We want to update the context object by using the updateDoc function of the context any time the documentation changes
<DocContext.Consumer>
{({ updateDoc }) => (
<Collapsible
trigger={name}
onTriggerOpening={() => updateDoc(cog)}
onTriggerClosing={() => updateDoc(defaultDoc)}>
Commands:
<ul>
{commands.map((command, index) => {
return (
<li key={index}>
<Command {...command} />
</li>
)
}
)}
</ul>
</Collapsible>
)}
</DocContext.Consumer>
)
}
in this case it causes doc to be set to what cog was which is a thing that has a name and documentation, which gets displayed. All of this without ever causing the CogSelector to be rerendered.
As per the reconciliation algorithm described here https://reactjs.org/docs/reconciliation.html.
In your parent you have first rendered <CogSelector .../> but later when the state is changed it wants to render <div> <CogSelector .../></div>... which is a completely new tree so react will create a new CogSelector the second time
So I am building a React app and got I think a decent idea pf whay I am doing. But I am looking to find how I can switch between components. Each component is its own individual js file.
App.js file:
import React from 'react';
import './App.css';
import MainPage from './mainpage'
function App() {
return (
<div className="App">
<h1>Welcome to Comix Nation </h1>
<MainPage />
</div>
);
}
export default App;
mainpage.js file:
import React from 'react';
import './App.css';
import CreateAccount from './createaccount.js'
import LogIn from './login.js'
import MainMenu from './mainmenu.js'
class MainPage extends React.Component {
constructor(props){
super(props);
this.state = {
currentPage: 'login'
};
}
getPage(currentPage){
const page ={
mainmenu: <MainMenu />,
createaccount: <CreateAccount />,
login: <LogIn />
};
return page[currentPage]
}
switchPage(currentPage){
this.setState({currentPage});
};
render(){
return (
<div>
<div>
<MainMenu switchPages={this.switchPage}/>
</div>
</div>
);
}
}
export default MainPage;
mainmenu.js file:
import React from 'react';
import './App.css';
class MainMenu extends React.Component {
constructor(props){
super(props);
this.state = {page: 'none'}
}
handleSelection(pageSelection){
this.props.switchPage(pageSelection);
}
render(){
return (
<div>
<h2 onClick={()=> this.handleSelection('createaccount')}>Click to create new account</h2>
<h2>Click to log in</h2>
</div>
);
}
}
export default MainMenu;
The idea is that I can click on either the create or login and get the appropriate js file to render.
so, from reading your code it sounds like you want to do routing (judging from your naming convention at least). There are a number of routing libraries you can use to render different pages if you want to use that. If you just want to switch out components, you've almost got it
class MainPage extends React.Component {
constructor(props){
super(props);
this.state = {
currentPage: 'login'
};
}
switchPage(currentPage){
this.setState({currentPage});
};
render(){
return (
<div>
<div>
{
this.state.currentPage === 'login' &&
<Login/>
}
{
this.state.currentPage === 'MainMenu' &&
<MainMenu/>
}
{
this.state.currentPage === 'SignUp' &&
<SignUp/>
}
</div>
</div>
);
}
}
The way react reads this is true and render this component some people prefer to use a ternary and return null but this is cooler imho 😎
There are several ways to do this, if you are trying to avoid react-router-dom you can implement this system fairly easy.
this.state = {
currentComponent: "",
}
this will allow you to keep track of what component is suppose to show. Put this in your controllers state.
showComponent = (component) => {
this.setState({currentComponent: component})
}
Put this in your main controller file, where you import your components that you will use.
Then you set up your components to display depending what is sent in.
let checkCurrentComponent = this.state.currentComponent;
Make a variable to check for easy checking.
{checkCurrentComponent === "topicList" ? (
<TopicTitles
showComponent={this.showComponent}
/>
) : checkCurrentComponent === "author" ? (
<TopicData
showComponent={this.showComponent}
/>
) : checkCurrentComponent === "commentForm" ? (
<CommentForm }
showComponent={this.showComponent}
/>
): null}
Then in your components you can use that function to pass in the name. Here is how I like to do that.
const handleCommentForm = (e, component) => {
e.preventDefault();
props.showComponent(component);
}
This will be at the top of my stateless function.
will bring up my comment form.
Then the button..
<button
className="btn btn-outline-none"
onClick={e => handleCommentForm(e, "commentForm")}
>
Add Comment
</button>
I retrieve an array of data through fetch from an API. In my React Component, when I use mapStateToProps and .map(), I am able to display the contents of the array. However, if I try to get just one element from the array like array[0], it keeps returning undefined.
/* HomePage class Component: Ascendent of Banner */
class HomePage extends Component {
componentWillMount() {
this.props.fetchMovies();
}
render() {
const movie = this.props.movies[0];
return (
<div>
<Banner movies={this.props.movies} movie={movie} />
<Movies movies={this.props.movies} />
</div>
);
}
}
HomePage.propTypes = {
fetchMovies: PropTypes.func.isRequired,
movies: PropTypes.array.isRequired
};
const mapStateToProps = state => ({
movies: state.movies.movies
});
export default connect(
mapStateToProps,
{ fetchMovies }
)(HomePage);
/* Banner class Component: Descendent of HomePage */
class Banner extends Component {
render() {
const movieList = this.props.movies.map(movie => {
return <li>{movie.title}</li>;
});
return (
<div style={styles.BannerContainer}>
<div style={styles.Banner}>
<div style={styles.BannerText}>
<h1 style={styles.BannerTextHeader}>{this.props.movie.title}</h1>
<p style={styles.BannerTextParagraph}>
Arthur Curry learns that he is the heir to the underwater kingdom
of Atlantis, and must step forward to lead his people and be a
hero to the world.
</p>
<ul>{movieList}</ul>
<Button content={"Check It Out"} />
</div>
<div style={styles.BannerImage} />
<div style={styles.BannerOverlay} />
</div>
</div>
);
}
}
export default Banner;
I expect this.props.movie.title to equal this.props.movies[0].title, but the actual output is an error saying cannot get title of undefined.
The reason is that this.props.movies is undefined on first render )until you make the call to fetchMovies).
Consider checking if it exists first like this:
class HomePage extends Component {
componentWillMount() {
this.props.fetchMovies();
}
render() {
if (this.props.movies && this.props.movies[0]) {
const movie = this.props.movies[0];
return (
<div>
<Banner movies={this.props.movies} movie={movie} />
<Movies movies={this.props.movies} />
</div>
);
} else {
<div>Loading...</div>;
}
}
}
Why don't you access
{this.props.movie.title}
Like
{this.props.movie[0].title}
Seems more logical to me. And this might be the solution.
correct me if i am wrong.
And could you also console.log {this.props.movie}
Initially movies may be empty array and since you are accessing zero index position you should check it’s length before accessing zero index.
Change
const movie = this.props.movies[0];
To
if(this.props.movies.length){
const movie = this.props.movies[0];
console.log(movie);
}
Since movies is always an array so checking directly it’s length will resolve the issue
"Using the React Children API" code example is not working, tried several syntax options, seems the problem is not quite clear.
http://developingthoughts.co.uk/using-the-react-children-api/
class TabContainer extends React.Component {
constructor(props) {
super();
this.state = {
currentTabName: props.defaultTab
}
}
setActiveChild = (currentTabName) => {
this.setState({ currentTabName });
}
renderTabMenu = (children) => {
return React.Children.map(children, child => (
<TabMenuItem
title={child.props.title}
onClick={() => this.setActiveChild(child.props.name)}
/>
);
}
render() {
const { children } = this.props;
const { currentTabName } = this.state;
const currentTab = React.Children.toArray(children).filter(child => child.props.name === currentTabName);
return (
<div>
{this.renderTabMenu(children)}
<div>
{currentTab}
</div>
</div>
);
}
}
When I changed code like this, it compiles finally
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
const TabMenuItem = ({ title, onClick }) => (
<div onClick={onClick}>
{title}
</div>
);
class TabContainer extends React.Component {
constructor(props) {
super();
this.state = {
currentTabName: props.defaultTab
}
}
setActiveChild = ( currentTabName ) => {
this.setState({ currentTabName });
}
renderTabMenu = ( children ) => {
return React.Children.map(children, child => (
<TabMenuItem
title={child.props.title}
onClick={() => this.setActiveChild(child.props.name)}
/>
))
}
render() {
const { children } = this.props;
const { currentTabName } = this.state;
const currentTab = React.Children.toArray(children).filter(child =>
child.props.name === currentTabName);
return (
<div>
{this.renderTabMenu(children)}
<div>
{currentTab}
</div>
</div>
);
}
}
ReactDOM.render(<TabContainer />, document.getElementById("root"));
Not quite experienced with JS and React, so my questions:
1) should this.setActiveChild be used as this.props.setActiveChild?
2) renderTabMenu = ( children ) or renderTabMenu = ({ children })
3) how to fill this page with some content? I don't see any physical children actually present =)
4) don't get the point why bloggers put the code with errors or which is difficult to implement, very frustrating for newcomers
5) any general guidance what can be not working in this example are welcome
Using React.Children or this.props.children can be a bit of a level up in your understanding of React and how it works. It'll take a few tries in making a component work but you'll get that aha moment at some point. In a nutshell.
this.props.children is an array of <Components /> or html tags at the top level.
For example:
<MyComponent>
<h1>The title</h1> // 1st child
<header> // 2nd child
<p>paragraph</p>
</header>
<p>next parapgraph</p> // 3rd child
</MyComponent>
1) should this.setActiveChild be used as this.props.setActiveChild?
Within the TabContainer any functions specified within it need to be proceeded with this. Within a react class this refers to the class itself, in this case, TabContainer. So using this.setActiveChild(). will call the function within the class. If you don't specify this it will try to look for the function outside of the class.
renderTabMenu = ( children ) or renderTabMenu = ({ children })
renderTabMenu is a function which accepts one param children, so call it as you would call it as a normal function renderTabMenu(childeren)
How to fill this page with some content? I don't see any physical children actually present =)
Here's where the power of the TabsContainer comes in. Under the hood, things like conditional rendering happen but outside of it in another component you specify the content. Use the following structure to render home, blog, and contact us tabs.
<TabsContainer defaultTab="home">
<Tab name="home" title="Home">
Home Content
</Tab>
<Tab name="blog" title="Blog">
Blog Content
</Tab>
<Tab name="contact" title="Contact Us">
Contact content
</Tab>
</TabsContainer>
I know how hard it is to make some examples work especially when you are starting out and are still exploring different concepts that react has to offer. Luckily there's stack overflow :).
Here's real live example to play around with, visit this CodeSandBox.
I'm trying to dynamically add Components (based on ID from an array) into my Presentational Component. I'm new to all this so there is a possibility I'm making it way too difficult for myself.
Here's the code of my Container Component:
class TemplateContentContainer extends Component {
constructor() {
super()
this.fetchModule = this.fetchModule.bind(this)
this.removeModule = this.removeModule.bind(this)
this.renderModule = this.renderModule.bind(this)
}
componentWillReceiveProps(nextProps) {
if(nextProps.addAgain !== this.props.addAgain) // prevent infinite loop
this.fetchModule(nextProps.addedModule)
}
fetchModule(id) {
this.props.dispatch(actions.receiveModule(id))
}
renderModule(moduleId) {
let AddModule = "Modules.module" + moduleId
return <AddModule/>
}
removeModule(moduleRemoved) {
console.log('remove clicked' + moduleRemoved)
this.props.dispatch(actions.removeModule(moduleRemoved))
}
render() {
return (
<div>
<TemplateContent
addedModule={this.props.addedModule}
templateModules={this.props.templateModules}
removeModule={this.removeModule}
renderModule={this.renderModule}
/>
</div>
)
}
}
and the code of the Presentational Component:
const TemplateContent = (props) => {
let templateModules = props.templateModules.map((module, index) => (
<li key={index}>
{props.renderModule(module)}
<button onClick={props.removeModule.bind(this, index)}>
remove
</button>
</li>
))
return (
<div>
<ul>
{templateModules}
</ul>
</div>
)
}
the renderModule function returns object, but when it's being passed to the presentational Component it doesn't work anymore (unless it's passed as className for example then it returns object)
I'm importing the modules from modules folder where I export them all into index.js file
import * as Modules from '../components/modules'
Hope it makes sense, any help would be highly appreciated.
Thanks a lot in advance!
I would recommend to restructure the files to make for an easier handling.
If your container components render would look like this:
render () {
return (
<div>
<ul>
{this.props.templateModules.map(module => (
<ChildComponent onRemove={this.removeModule} module={module} />
)}
</ul>
</div>
)
}
Your child component can just handle the remove click and the displaying of the module data
EDIT:
My bad, I just misunderstood your problem.
I would map the ids to the according components instead of concatenating the name of the Component you want to render, so your container component would look something like this:
getChildComponent (id) {
const foo = {
foo: () => {
return <Foo onRemove={this.removeModule} />
},
bar: () => {
return <Bar onRemove={this.removeModule} />
}
}
return foo[id]
}
render () {
return (
<div>
<ul>
{this.props.templateModules.map(module => (
{this.getChildComponent(module.id)()}
)}
</ul>
</div>
)
}
Also you should maybe have a look at react-redux and move your dispatches to react-redux containers.