in many of my components I am fetching API data and therefor I need to wait until that data was loaded. Otherwise I am getting errors because some methods are, of course, not available.
My api query looks like this
componentDidMount() {
prismicApi(prismicEndpoint).then((api) =>
api.form('everything')
.ref(api.master())
.query(Prismic.Predicates.at("my.page.uid", this.props.params.uid))
.submit((err, res) => {
if (res.results.length > 0) {
this.setState({doc: res.results[0]});
} else {
this.setState({notFound: true});
}
}))
}
For that I've created this structure that I have been using in all of these documents:
render() {
if (this.state.notFound) {
return (<Error404 />);
} else if (this.state.doc == null || !this.state.doc) {
return (<Loading />);
} else {
return (
<div className="page">
{this.state.doc.getSliceZone('page.body').slices.map(function(slice, i){
return (<SliceZone slice={slice} key={i} />)
})}
</div>
)
}
}
I wanted to move this into a component called Document that looks like this here:
export default class Document extends React.Component {
static defaultProps = {
doc: null,
notFound: false
}
static propTypes = {
doc: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]),
notFound: React.PropTypes.bool.isRequired
}
render() {
if (this.props.notFound) {
return (<Error404 />);
} else if (this.props.doc == null || !this.props.doc) {
return (<Loading />);
} else {
return (
<div className="page">
{this.props.children}
</div>
)
}
}
}
and then I tried to use it like this here:
<Document doc={this.state.doc} notFound={this.state.notFound}>
{this.state.doc.getSliceZone('page.body').slices.map(function(slice, i){
return (<SliceZone slice={slice} key={i} />)
})}
</Document>
Though on the second example the error messages are showing up quickly (until the data is loaded) and then disappear. What am I doing wrong? Why is the first example working and the second doesnt?
try this
<Document doc={this.state.doc} notFound={this.state.notFound}>
{ this.state.doc && this.state.doc.getSliceZone('page.body').slices.map(function(slice, i){
return (<SliceZone slice={slice} key={i} />)
})}
</Document>
in your variant, you see an error becuase this.state.doc is null, untill data is loaded, and you see null reference exception, looks like.
In 1st case, it does not calculate, in 2nd case it calculates first and then sent as a parameter "children" to your Document control
Related
I'm working on React with Symfony API and when I connect to my app, I've got a role defined by Symfony
It returns this if I'm an admin : ["ROLE_USER", "ROLE_ADMIN"]
It returns this if I'm a moderator : ["ROLE_USER", "ROLE_MODERATOR"]
It returns this if I'm a user : ["ROLE_USER"]
Currently my code is working fine and if I'm a user, it shows the user view, if I'm a moderator it shows the moderator view etc.
So my question is : Is there a better way to create a condition that will render the good component in function of my user role ?
render()
{
let content = "";
if (this.props.auth.user.roles.includes("ROLE_ADMIN")) {
content = <NavAdminDashboard />;
} else if (this.props.auth.user.roles.includes("ROLE_MODERATOR")) {
content = <NavModeratorDashboard />;
} else {
content = <NavUserDashboard />;
}
return (
<Fragment>
{content}
</Fragment>
)
}
I have checked this : Render component based on a variable - reactjs
It is better than my code but it only works if my roles render as string and not as array like my code.
You can achieve this in two ways
The first one is a little cleaner.
render(){
const {roles} = this.props.auth.user;
return (
<React.Fragment>
{ roles.include("ROLE_ADMIN") && <NavAdminDashboard /> }
{ roles.include("ROLE_MODERATOR") && <NavModeratorDashboard /> }
{ !roles.include("ROLE_ADMIN") && !roles.include("ROLE_MODERATOR) && <NavUserDashboard /> }
</React.Fragment>
)
}
You can also do that by creating two methods isAdmin and isModerator:
isAdmin = () => {
return this.props.auth.user.roles.include("ROLE_ADMIN");
}
isModerator = () => {
return this.props.auth.user.roles.include("ROLE_MODERATOR");
}
render() {
return (
<React.Fragment>
{ this.isAdmin() && <NavAdminDashboard /> }
{ this.isModerator() && <NavModeratorDashboard /> }
{ !this.isAdmin() && !this.isModerator() && <NavUserDashboard /> }
</React.Fragment>
)
}
Or you can add a isUser method to check if its only user
isUser = () => {
const {roles} = this.props.auth.user;
return roles.include("ROLE_USER") && roles.length === 1;
}
render() {
return (
<React.Fragment>
{ this.isAdmin() && <NavAdminDashboard /> }
{ this.isModerator() && <NavModeratorDashboard /> }
{ this.isUser() && <NavUserDashboard /> }
</React.Fragment>
)
}
I think your code is fine and doesn't necessary need to change. But I personally move the role logic either to external functions (that can be unit tested) or methods on the component. Eg:
get isAdmin() {
return this.props.roles.include('ADMIN');
}
get isUser() {
return !this.props.roles.some(role => role !== 'USER');
}
render() {
return <>
{this.isAdmin && <Admin />}
{this.isUser && <User />}
</>
}
Another alternative is to move the parsing of roles to a helper function and map the array to props. Eg:
<Component isAdmin={hasAdminRole(roles)} />
Both of these are nicer solutions if you ask me. But in the end, as long as the code works it might be good enough. You can always go back and refactor later.
I am trying to get No list items only when there's nothing coming from the backend. Right now, onload, I get the loading spinner and No List items before I fetch the data.
So, I thought I would add a timeout to deal with this so that it will only show up after the fetching is done, and there are no items
getList() {
if(this.state.list.length != 0){
return (this.state.list.map(data => {
return <div data={data} key={data.id}/>
}))
}else{
return <div>No List items</div>
}
}
render() {
return (
<div>
<Spinner active={this.state.active} />
<div>{setTimeout(this.getList, 1000)}</div>
</div>
);
}
}
When i use this, I am getting numbers on the browser. The active state of spinner changes on componentDidMount to false
That's what setTimeout returns: an id number, which you can use later if you want to cancel the timeout.
The render method is synchronous. If you want to render nothing for the case where you don't have data, then you can have render return null. Then in componentDidMount, do any async work you need, and when it completes, call this.setState to update the state and rerender (this time without a null)
class Items extends React.Component {
constructor(props) {
super();
this.state = {
active: true,
is_loading: false,
}
}
componentDidMount() {
this.timeout_number = setTimeout(() => {
this.setState({
active: false,
is_loading: true
});
}, 1000);
}
componentWillUnmount() {
clearTimeout(this.timeout_number);
}
getList() {
if(this.state.list.length)
return this.state.list.map(data => <div data={data} key={data.id}/>)
else
return <div>No List items</div>
}
render() {
return (
<div>
<Spinner active={this.state.active} />
{this.state.is_loading
? this.getList()
: null}
</div>
);
}
}
export default Items;
Don't use a timeout here. I would just set the initial state of list to null. Then just flip your logic so that it is:
getList() {
if(this.state.list && this.state.list.length == 0){
return <div> No List items </div>
}else{
return (this.state.list.map(data => {
return <div data={data} key={data.id}/>
}))
}
}
There are 100 ways to solve this but this is the easiest based on your code. ALso don't forget the difference between != and !==.
I have the following react code:
{myArray.map(arr => {
return ( <MyComponent title={arr.ttile} /> )
})}
I would like to call a Loading component while the map() is not completely finished. Is it possible to do that? If yes, how would I do that?
If you are getting your data from an API, you might want to render the data as usual, but you can get the data in the componentDidMount hook instead, and e.g. keep an additional piece of state isLoading which you can use in the render method to decide if you should show a loading component.
Example
function getBooks() {
return new Promise(resolve => {
setTimeout(() => resolve([{ title: "foo" }, { title: "bar" }]), 1000);
});
}
function MyComponent(props) {
return <div> {props.title} </div>;
}
class App extends React.Component {
state = { books: [], isLoading: true };
componentDidMount() {
getBooks().then(books => {
this.setState({ books, isLoading: false });
});
}
render() {
const { isLoading, books } = this.state;
if (isLoading) {
return <div> Loading... </div>;
}
return (
<div>
{this.state.books.map(book => <MyComponent title={book.title} />)}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
If you want to actually be able to see the components being loaded behind/under the loading indicator, then it would be more challenging and would probably need more work than this proposed solution. But if you just want a loading indicator to show while the .map() prototype function is working, I believe this would do the trick:
constructor(props) {
super(props);
this.state = { loadingIndicator : null };
}
getArrayOfMyComponents() {
return myArray.map((arr, index) => {
if (index === 0) {
const loadingIndicator = <Loading/>;
this.setState({ loadingIndicator : loadingIndicator });
} else if (index === myArray.length - 1) {
this.setState({ loadingIndicator : null });
}
return ( <MyComponent title={arr.title} /> );
});
}
render() {
const arrayOfMyComponents = this.getArrayOfMyComponents();
return (
<div>
{this.state.loadingIndicator}
{arrayOfMyComponents}
</div>
);
}
Array.prototype.map() is really just a fancier version of Array.prototype.forEach(). So we can leverage that fact to launch the display of the loading indicator on the first iteration and remove it on the last.
you can have a boolean in a state, and just before you start array map put boolean true and run another code o component render, and then when array maps end you put that state to false, for redux im using state fetch start, fetching, fetched, and then you can take the control of situation
I have a Dashboard component that renders an array of cards with data fetched from a backend server. Users can create additional cards by submitting a form, which then redirects them back to the dashboard page.
My issue is that when the form is submitted, a javascript error 'cannot read property "includes" of undefined' is thrown and the dashboard does not render. If I manually refresh the page, the list renders as expected with the new card. I use Array.includes method to filter the cards based on the filterText state value. Does this error happen because the data has not been fetched when render is called? If so, how can I force the component to wait until there is data before rendering? Please see the components and redux action below.
const CardList = (props) => {
const cards = props.cards.map(({ _id, title}) => {
return (
<Card key={_id} title={title} />
)
});
return (
<div className="container">
<input onChange={ (e) => props.handleChange(e.target.value) } />
<div className="row">
{cards}
</div>
</div>
);
}
export default CardList;
export class Dashboard extends Component {
constructor(props) {
super(props);
this.state = {
filterText: ''
}
}
componentDidMount() {
this.props.fetchCards();
}
handleChange = (filterText) => {
this.setState({filterText});
}
render() {
const cardList = this.props.cards.filter(card =>
card.title.includes(this.state.filterText.trim().toLowerCase())
);
return (
<div>
<CardList cards={cardList}
handleChange={filterText => this.handleChange(filterText)} />
</div>
);
}
};
function mapStateToProps({ cards: { cards }}) {
return {
cards,
}
}
export default connect(mapStateToProps, {fetchCards})(Dashboard);
export class SurveyForm extends Component {
render() {
return (
<div>
<form>
<Field component={CardField} type="text"
label={'title'} name={'title'} key={'title'} />
<Button type="submit" onClick={() => submitCard(formValues, history)}>Next</Button>
</form>
</div>
);
}
}
REDUX ACTION DISPATCHER:
export const submitCard = (values, history) => async dispatch => {
const res = await axios.post('/api/cards', values);
try {
dispatch({ type: SUBMIT_CARD_SUCCESS, payload: res.data });
dispatch({ type: FETCH_USER, payload: res.data })
}
catch(err) {
dispatch({ type: SUBMIT_CARD_ERROR, error: err });
}
history.push('/cards');
}
Similar to what #JasonWarta mentioned, it's worth noting that React does not render anything when false, null, or undefined is returned, so you can usually use && to be more succinct than using the conditional ("ternary") operator:
render() {
return this.props.cards && (
<div>
<CardList
cards={this.props.cards.filter(card => card.title.includes(this.state.filterText.trim().toLowerCase())}
handleChange={filterText => this.handleChange(filterText)}
/>
</div>
);
}
Because && short-circuits, the latter part won't be evaluated so you can avoid TypeErrors, and the component will also render no content (same as when you return null).
I've used ternary operators in this kind of situation. You may need to adjust the check portion of the pattern, depending on what your redux pattern is returning. null value is returned if this.props.cards is falsey.
render() {
return (
{this.props.cards
?
<div>
<CardList
cards={this.props.cards.filter(card => card.title.includes(this.state.filterText.trim().toLowerCase())}
handleChange={filterText => this.handleChange(filterText)}
>
</CardList>
</div>
:
null
}
);
}
As an alternative to other answers you can return something else suitable if there is no data in your render function with an if statement. I prefer moving functions like your filter one outside of render. Maybe one other (better?) approach is doing that filter in your mapStateToProps function.
Also, if I'm not wrong you don't need to pass anything to your handleChange function. Because you are getting filterText back from CardList component then setting your state.
cardList = () => this.props.cards.filter(card =>
card.title.includes(this.state.filterText.trim().toLowerCase()));
render() {
if ( !this.props.cards.length ) {
return <p>No cards</p>
// or return <SpinnerComponent />
}
return (
<div>
<CardList cards={this.cardList()}
handleChange={this.handleChange} />
</div>
);
}
Have been facing issue while iterating through a list and printing elements in React.
The React Code is:
import React from 'react';
import ReactDOM from 'react-dom';
class NewComponent extends React.Component {
constructor(props){
super(props);
this.state = {myData: []}
}
componentWillMount(){
let data = document.getElementById('demo').innerHTML;
data = JSON.parse(data);
this.setState({myData: data});
}
render() {
return this.state.myData.map((item) => {
return (
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
);
});
}
}
ReactDOM.render(
<NewComponent />,
document.getElementById('demo')
)
And I'm getting an error of:
bundle.js:830 Uncaught Error: NewComponent.render(): A valid React element
(or null) must be returned. You may have returned undefined, an array or
some other invalid object.
I'm pretty sure there is some issue with returns in the render function of the
Not sure what is the issue though.
EDITS
I have made the following edits, the error is not there anymore but nothing is rendering.
renderList() {
console.log("Running");
return this.state.myData.map((item) => {
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
});
}
render() {
console.log(this.state.myData);
if(this.state.myData.length)
return <div>{this.renderList()}</div>
else
return <div>Loading...</div>
}
In Chrome console I'm getting:
(2) [{…}, {…}]
0:{_id: {…}, description: "hello", title: "sankit"}
1:{_id: {…}, description: "lets add some thing new", title: "hi"}
length:2
_proto_:Array(0)
Running
what you can do is extract your js code from the render method in a separate method like so:
renderList() {
return this.state.myData.map((item) => {
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
})
}
then in your render method:
render() {
if(this.state.myData.length){
return (
<div>{this.renderList()}</div>
);
}
else
{
return (
<div>Loading...</div>
);
}
}
You can wrap it with root element like div,
React ver 15 render functions supports only returning one element.
render() {
<div>{this.state.myData.map((item) =>
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
)}</div>
}
}
Change like this, While you using map should use key attribute for index
makeUI() {
if(!this.state.myData.length)
return
return this.state.myData.map((item, index) => {
return (
<div key={index}>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
)
})
}
render() {
return (<div>
{ this.makeUI() }
</div>
)
}
I think you are missing the return in renderList -> .map
This should work.
renderList() {
return this.state.myData.map((item) => {
return (
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
);
});
}
render() {
if(this.state.myData.length){
return (
<div>{this.renderList()}</div>
);
}
else {
return (
<div>Loading...</div>
);
}
}