Specifying type in Typescript - reactjs

I am using NextJs with Typescript.
Only when I add the "any" keyword, my code renders properly otherwise it gives me errors for my post._id, post.title and post.body.
Problem: What is the specific type for displayblog so that I do not set it to any? Also, how can I define my states and props for the code below?
function dateToString(date: Date): string {
return (
`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}` +
` ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
);
}
export default class LA extends React.Component {
state = {
title: "",
};
componentDidMount = () => {
this.Post();
};
getBlogPost = () => {
axios
.get("/api")
.then(({ data }) => {
const reverseData = new Array();
.catch(error => {
alert("Error: ERROR");
});
};
return posts.map((post, index) => (
<div key={index}>
<Card>
<Title>
{" "}
</Title>
{/* <p>{blog.date}</p>
<p>{blog.name}</p> */}
<FullName>{`${blog.name} | ${dateToString(
currentDateTime
)}`}</FullName>
<Line />
<Question>{blog.body}</Question>
</Card>
</div>
));
};
render() {
return (
<div>
<Container>
<Headers />
<div className="blog">{this.displayBlogPost(this.state.posts)}</div>
</Container>
</div>
);
}
}
This is the line on which I want to change any to a specific type.
displayBlogPost = (posts : Array < any >) => {

Assuming your posts array based on your code in question, create an interface called "Post".
Create separate interfaces for props and state and define as per your format. In this case posts is an array, so I am defining an interface in prior and use it in the state. Similarly if you have got any props, define a format for them too and use it. Also don't forget to use them along while extending the class.
interface Post {
name: string;
body: string;
title: string;
_id: number | string;
}
interface PostState {
posts: Post[];
}
//Define based on your need
interface PostProps {}
export default class PostList extends React.Component<PostProps, PostState> {
state = {
posts: []
};
componentDidMount = () => {
this.getBlogPost();
};
getBlogPost = () => {
axios
.get("/api")
.then(({ data }) => {
const reverseData = new Array();
for (let datetime = data.length - 1; datetime >= 0; datetime--) {
reverseData.push(data[datetime]);
}
this.setState({ posts: reverseData });
})
.catch(error => {
alert("Error: there was an error processing your request");
});
};
displayBlogPost = (posts: Post[]) => {
const currentDateTime = new Date();
if (!posts.length) return null;
return posts.map((post, index) => (
<div key={index}>
<Card>
<Title>
{" "}
<Link href={`/post?_id=${post._id}`}>
<a>{post.title}</a>
</Link>
</Title>
{/* <p>{post.date}</p>
<p>{post.name}</p> */}
<FullName>{`${post.name} | ${dateToString(
currentDateTime
)}`}</FullName>
<Line />
<Question>{post.body}</Question>
</Card>
</div>
));
};
render() {
return (
<div>
<Container>
<Headers />
<div className="blog">{this.displayBlogPost(this.state.posts)}</div>
</Container>
</div>
);
}
}

Related

Comment reply system with React Quill and Firebase Firestore

I'm making a comment system with React Quill as my editor and Firebase Firestore. Each comment post gets stored in firestore. Each stored comment has a reply button, and when clicked, the editor should be populated with the comment content I want to reply to. Basically I need to populate my editor with the content stored in firestore database. Here's a screenshot as to watch I want to achieve:
Comment reply
Here's some code from the comment editor component
class NewComment extends Component {
constructor(props) {
super(props);
this.state = {
comment: {
commentID: "",
content: "",
createDate: new Date(),
featureImage: "",
isPublish: "True",
createUserID: "",
},
};
}
...
onChangeCommentContent = (value) => {
this.setState({
comment: {
...this.state.comment,
content: value,
},
});
};
...
render() {
return (
<Container>
<Row>
<Col xl={9} lg={8} md={8} sn={12}>
<h2 className={classes.SectionTitle}>Comment</h2>
<FormGroup>
<ReactQuill
ref={(el) => (this.quill = el)}
value={this.state.comment.content}
onChange={(e) => this.onChangeCommentContent(e)}
theme="snow"
modules={this.modules}
formats={this.formats}
placeholder={"Enter your comment"}
/>
</FormGroup>
</Col>...
The reply button is in a different component where I render the stored comments. Tell me if you need the full code from the components.
Here is a simple example on how to pass on information between two components via the parent component using function components:
// Index.js
const MyComponent = () => {
const [replyValue, setReplyValue] = useState("");
const onClick = (value) => {
setReplyValue(value);
};
return (
<>
<Comment value="This is a reply" onClick={onClick} />
<Comment value="This is another reply" onClick={onClick} />
<CreateReply quoteValue={replyValue} />
</>
);
};
// Comment.js
export const Comment = ({ value, onClick }) => {
return (
<div className="comment" onClick={() => onClick(value)}>
{value}
</div>
);
};
// CreateReply.js
export const CreateReply = ({ quoteValue = "" }) => {
const [value, setValue] = useState("");
useEffect(() => {
setValue(quoteValue);
}, [quoteValue]);
const onValueUpdated = (newValue) => {
if (newValue !== value) {
setValue(newValue);
}
};
return (
<>
<ReactQuill value={value} onChange={onValueUpdated} />
</>
);
};
Here is the same example using class components:
// Index.js
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = {
replyValue: ""
};
}
onClick = (value) => {
this.setState({
replyValue: value
});
};
render() {
return (
<>
<Comment value="This is a reply" onClick={this.onClick} />
<Comment value="This is another reply" onClick={this.onClick} />
<CreateReply quoteValue={this.state.replyValue} />
</>
);
}
}
// Comment.js
export class Comment extends React.Component {
render() {
return (
<div
className="comment"
onClick={() => this.props.onClick(this.props.value)}
>
{this.props.value}
</div>
);
}
}
// CreateReply.js
export class CreateReply extends React.Component {
constructor(props) {
super(props);
this.onValueUpdated = this.onValueUpdated.bind(this);
this.state = {
value: props.quoteValue
};
}
componentDidUpdate(prevProps) {
if (this.props.quoteValue !== prevProps.quoteValue) {
this.setState({
value: this.props.quoteValue
});
}
}
onValueUpdated = (newValue) => {
if (newValue !== this.state.value) {
this.setState({
value: newValue
});
}
};
render() {
return (
<>
<ReactQuill value={this.state.value} onChange={this.onValueUpdated} />
</>
);
}
}

How to update data between components?

Image of the consoleI'm trying to create a website using the movie DB API. I've created a carousel with some movies and want to open a new page with comprehensive information about a movie by clicking on the movie poster. I'm using componentDidMount to fetch data in one component to create a UI and I pass a movie ID to another component where I use componentWillReceiveProps to fetch another data by using the ID. It worked until I started using state, so now it shows two arrays in the console before I click on a movie poster and when I click on a poster it shows an array and loads a movie data from API then if I click on another poster it loads two different objects with the previous and current movie. I found out that componentWillReceiveProps is dangerous to use but componentDidUpdate works in the same manner.
The main idea is when a user clicks on a poster it gets its id and sends it to another component where the id goes to a link
with complete information about the movie. Are there any patterns to achieve it?
class Data extends Component {
state = {
movies: [],
movieId: null
};
onClick = e => {
this.setState({
movieId: e.target.id
});
console.log(e.target.id);
};
componentDidMount() {
fetch(url)
.then(res => res.json())
.then(data => {
let movies = data.results.map(item => {
return (
<Link to="/movieInfo">
<div className="overlay" onClick={this.onClick}>
<img
src=
{`https://image.tmdb.org/t/p/w500/${item.poster_path}`}
alt={item.title}
id={item.id}
/>
</div>
</Link>
);
});
this.setState({
movies: movies
});
})
.catch(err => console.log(err));
}
render() {
const { movies, movieId } = this.state;
return (
<div className="carousel">
<Slider movie={movies} />
<div className="notShow">
<AdditionalInfo id={movieId} />
</div>
</div>
);
}
}
class AdditionalInfo extends Component {
state = {
movie: []
};
componentDidUpdate(prevProps) {
if (prevProps.id !== null && prevProps.id !== this.props.id) {
fetch(
`https://api.themoviedb.org/3/movie/${
prevProps.id
}?api_key=81f382d33088c6d52099a62eab51d967&language=en-US`
)
.then(res => res.json())
.then(data =>
this.setState({
movie: data
})
);
} else {
return null;
}
}
render() {
const { movie } = this.state;
console.log(movie);
return (
<div className="movieInfo-container">
{/* <section className="title" />
<section className="cast">{movie.id}</section> */}
work
</div>
);
}
}
let movieArr = [];
class Slider extends Component {
state = {
currentIndex: 0,
translateValue: 0
};
createNestedArr = () => {
while (this.props.movie.length) {
movieArr.push(this.props.movie.splice(0, 5));
}
return movieArr.map((item, i) => {
return <Slide key={i} movieGroup={item} />;
});
};
nextPic = () => {
if (this.state.currentIndex === movieArr.length - 1) {
return this.setState({
currentIndex: 0,
translateValue: 0
});
}
this.setState(prevState => ({
currentIndex: prevState.currentIndex + 1,
translateValue: prevState.translateValue - this.slideWidth()
}));
};
prevPic = () => {
if (this.state.currentIndex === movieArr.length + 1) {
return this.setState({
currentIndex: 0,
translateValue: 0
});
} else if (this.state.currentIndex === 0) {
return this.setState({
currentIndex: 0,
translateValue: 0
});
}
this.setState(prevState => ({
currentIndex: prevState.currentIndex - 1,
translateValue: prevState.translateValue + this.slideWidth()
}));
};
slideWidth = () => {
return document.querySelector(".new-releases-slide").clientWidth;
};
render() {
return (
<React.Fragment>
<div
className="movie-carousel"
style={{
transform: `translateX(${this.state.translateValue}px)`,
transition: "transform ease-out 0.45s"
}}
>
{this.createNestedArr()}
</div>
<LeftArrow prevPic={this.prevPic} />
<RightArrow nextPic={this.nextPic} />
</React.Fragment>
);
}
}
const Slide = props => {
const { movieGroup } = props;
return <div className="new-releases-slide">{movieGroup}</div>;
};
Use componentDidMount in your AdditionalIno component. You need to pass the id of the clicked movie to MovieInfo component. This <Link to="/movieInfo"> needs to <Link to={'/movieInfo/${item.id}'}> and in your MovieInfo component access the id using const { id } = this.props.match.params;.
import React, { Component } from 'react';
import Loader from "react-loader-spinner";
class AdditionalInfo extends Component {
state = {
movie: [],
isLoading: true,
};
componentDidMount = () => {
const { id } = this.props;
if (!id) {
return;
}
fetch(
`https://api.themoviedb.org/3/movie/${id}?api_key=81f382d33088c6d52099a62eab51d967&language=en-US`
)
.then(res => res.json())
.then(data =>
this.setState({
movie: data,
isLoading: false,
})
);
}
render() {
const { movie } = this.state;
return (
<div className="movieInfo-container">
{this.state.isLoading
? <Loader type="Puff" color="#00BFFF" height="100" width="100" />
: <div><section className="title" />
<h1>{movie.title}</h1>
<section className="cast">ID: {movie.id}</section>
<h2>Overview</h2>
<p>{movie.overview}</p></div>
}
</div>
);
}
}
export default AdditionalInfo;
then in your Data component change your componentDidMount
componentDidMount = () => {
fetch(url)
.then(res => res.json())
.then(data => {
let movies = data.results.map(item => {
return (
<Link to={`/movieInfo/${item.id}`}>
<div className="overlay" onClick={this.onClick}>
<img
src=
{`https://image.tmdb.org/t/p/w500/${item.poster_path}`}
alt={item.title}
id={item.id}
/>
</div>
</Link>
);
});
this.setState({
movies: movies
});
})
.catch(err => console.log(err));
}
In your MovieInfo do something like
class MovieInfo extends Component {
render() {
const {id} = this.props.match.params;
return (
<div>
<AdditionalInfo id={id} />
</div>
)
}
}
Your router should be like
<Route path="/movieInfo/:id" exact component={MovieInfo} />
Working Demo

How to pass variables when calling components

I'm building a mini app and I want to get it cleaner.
So basically I want to have 3 components : App, List and Person.
Here is the code :
App.js
import React, { Component } from "react";
import List from './List';
class App extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
search: '',
currentPage: 1,
todosPerPage: 3
};
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
this.fetchData();
}
fetchData = async () => {
const response = await fetch(API);
const json = await response.json();
this.setState({ results: json.results });
};
handleClick(event) {
this.setState({
currentPage: Number(event.target.id)
});
}
updateSearch(event) {
this.setState({ search: event.target.value.substr(0, 20) });
}
render() {
return (
<List />
);
}
}
export default App;
List.js
import React, { Component } from 'react';
import Person from './Person';
class List extends Component {
render() {
const { results, currentPage, todosPerPage } = this.state;
const indexOfLastTodo = currentPage * todosPerPage;
const indexOfFirstTodo = indexOfLastTodo - todosPerPage;
const currentTodos = results.slice(indexOfFirstTodo, indexOfLastTodo).filter(item => {
return item.name.toLowerCase().indexOf(this.state.search) !== -1;
});
const renderTodos = currentTodos.map((item, index) => {
return (
<Person item={this.state.item} index={this.state.index}/>
);
});
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(results.length / todosPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li className="page-link" key={number} id={number} onClick={this.handleClick} style={{cursor: "pointer"}}>{number}</li>
);
});
return (
<div className="flex-grow-1">
<h1>Personnages de Star Wars</h1>
<form className="mb-4">
<div className="form-group">
<label>Rechercher</label>
<input
className="form-control"
type="text"
placeholder="luke skywalker..."
value={this.state.search}
onChange={this.updateSearch.bind(this)}
/>
</div>
</form>
<div className="row mb-5">{renderTodos}</div>
<nav aria-label="Navigation">
<ul id="page-number" className="pagination justify-content-center">{renderPageNumbers}</ul>
</nav>
</div>
);
}
}
export default List;
Person.js
import React, { Component } from 'react';
function Person(item, index) {
return (
<div className="col-lg-4 mb-4" key={index}>
<div className="card">
<div className="card-header">
<h4 className="mb-0">{item.name}</h4>
</div>
<div className="card-body">
<h5 className="card-title">Caractéristiques</h5>
<ul>
<li>Année de naissance : {item.birth_year}</li>
<li>Taille : {item.height} cm</li>
<li>Masse : {item.mass}</li>
<li>Couleur des yeux : {item.eye_color}</li>
<li>Couleur de cheveux : {item.hair_color}</li>
<li>Couleur de peau : {item.skin_color}</li>
</ul>
Sa fiche
</div>
</div>
</div>
)
}
export default Person;
My issue is that I get TypeError: Cannot read property 'results' of null when rendering.
Is it possible to have variable go into every file if I define them all in App.js ?
You are not passing the data the correct way. Try this:
In App.js pass to List component the needed data:
render() {
return (
<List data={this.state}/>
);
}
Then in render() method in List.js get the passed data prop, then extract the data from there:
render() {
const { data } = this.props;
const { results, search, currentPage, todosPerPage } = data;
// ...
// in currentTodos function dont use this.state.search but just "search", that we got above from the data variable
// ...
// also your renderTodos should look like this - use the item and index variables
const renderTodos = currentTodos.map((item, index) => {
return (
<Person item={item} index={index}/>
);
});
// ...
}
So your List.js should look like this:
import React, { Component } from 'react';
import Person from './Person';
class List extends Component {
render() {
// get the data
const { data } = this.props;
// get the properties
const { results, search, currentPage, todosPerPage } = data;
const indexOfLastTodo = currentPage * todosPerPage;
const indexOfFirstTodo = indexOfLastTodo - todosPerPage;
const currentTodos = results.slice(indexOfFirstTodo, indexOfLastTodo).filter(item => {
// use "search" variable
return item.name.toLowerCase().indexOf(search) !== -1;
});
const renderTodos = currentTodos.map((item, index) => {
return (
// use item and index
<Person item={item} index={index}/>
);
});
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(results.length / todosPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li className="page-link" key={number} id={number} onClick={this.handleClick} style={{cursor: "pointer"}}>{number}</li>
);
});
return (
<div className="flex-grow-1">
<h1>Personnages de Star Wars</h1>
<form className="mb-4">
<div className="form-group">
<label>Rechercher</label>
<input
className="form-control"
type="text"
placeholder="luke skywalker..."
value={search} // use search variable here too
onChange={this.updateSearch.bind(this)}
/>
</div>
</form>
<div className="row mb-5">{renderTodos}</div>
<nav aria-label="Navigation">
<ul id="page-number" className="pagination justify-content-center">{renderPageNumbers}</ul>
</nav>
</div>
);
}
}
export default List;
And your function in Person.js should have the following declaration, because the parameters are extracted from the passed props:
function Person({item, index}) {
// ...
}
You can use pass variables in your props of <List /> component by passing state inside render function of App.js while calling <List /> like this
render() {
//Passing Data inside props
<List data={this.state} />
}
and inside your List.js, You can access the data variable
const { results, currentPage, todosPerPage } = this.props.data;

How to show/hide an item of array.map()

I want to show/hide a part of JSX depending on isCommentShown state property. But as this part is inside a map loop isCommentShown acts for all mapped items not only the current one. So when I toggleComment every comment inside a loop is shown/hidden. I imagine this can be solved by moving everything into a separate component because every component has its own state. But I wonder if I can can solve this without that.
const SearchResults = () => {
const [isCommentShown, setIsCommentShown] = useState(false);
const toggleComment = () => {
setIsCommentShown(!isCommentShown);
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={toggleComment}/> : null }
<div>{obj.text}</div>
{ isCommentShown ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
You could use the useState hook to create an object that will keep all the search result ids as keys and a boolean value indicating if the comment should be shown or not.
Example
const { useState, Fragment } = React;
const SearchResults = props => {
const [shownComments, setShownComments] = useState({});
const toggleComment = id => {
setShownComments(prevShownComments => ({
...prevShownComments,
[id]: !prevShownComments[id]
}));
};
return (
<Fragment>
{props.search_results.map(obj => (
<div key={obj.id}>
{obj.comment ? (
<button onClick={() => toggleComment(obj.id)}>Toggle</button>
) : null}
<div>{obj.text}</div>
{shownComments[obj.id] ? <p>{obj.comment}</p> : null}
</div>
))}
</Fragment>
);
};
ReactDOM.render(
<SearchResults
search_results={[
{ id: 0, text: "Foo bar", comment: "This is rad" },
{ id: 1, text: "Baz qux", comment: "This is nice" }
]}
/>,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Instead of storing true or false, you must store the comment id to show provided you only want to show one comment at a time. Its important to uniquely identify the item to be expanded
const SearchResults = () => {
const [commentShown, setCommentShown] = useState({});
const toggleComment = (id) => {
setCommentShown(prev => Boolean(!prev[id]) ? {...prev, [id]: true} : {...prev, [id]: false});
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={() => toggleComment(obj.id)}/> : null }
<div>{obj.text}</div>
{ commentShown[id] ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
If at all you need to open multiple comments simultaneously you can maintain a map of open ids
const SearchResults = () => {
const [commentShown, setCommentShown] = useState('');
const toggleComment = (id) => {
setCommentShown(prev => prev.commentShown !== id? id: '');
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={() => toggleComment(obj.id)}/> : null }
<div>{obj.text}</div>
{ commentShown === obj.id ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
Use the id to target the toggle on the comment you want.
More precisely, use the state to store the show/hide values, and pass the id to the onclick event to precise which comment to toggle. This should do the job:
class SearchResults extends React.Component {
constructor(props) {
super(props);
this.state = {};
for (let result of props.search_results) {
this.state[`${result.id}IsShown`] = true;
}
}
toggleComment(id) {
let key = `${result.id}IsShown`;
this.setState({[key]: !this.state[key]});
}
render() {
return (
<>
{this.props.search_results.map(result =>
<div key={result.id}>
{
result.comment
? <img onClick={() => toggleComment(result.id)}/>
: null
}
<div>{result.text}</div>
{ isCommentShown ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
}
}

Child component not updating in React

Help! My child component is not updating in my react app!
I want to bring cartNumber to the page component which then is passed onto header component but the number doesn't even show up!
Parent component
class Shop extends Component {
constructor(props) {
super(props);
this.state = {
merchants: [],
error: null,
loading: true,
order: []
};
}
componentWillMount() {
Meteor.call("merchants.getMerchants", (error, response) => {
if (error) {
this.setState(() => ({ error: error }));
} else {
this.setState(() => ({ merchants: response }));
}
});
}
componentDidMount() {
setTimeout(() => this.setState({ loading: false }), 800); // simulates loading of data
}
goBack = () => this.props.history.push("/");
goCart = () => {
try {
Orders.insert(this.state.order), this.props.history.push("/cart");
} catch (error) {
throw new Meteor.Error("there was an error", error);
}
};
onAddToCart(cartItem) {
let { order } = this.state;
order.push(cartItem);
console.log(order.length);
}
render() {
const { loading } = this.state;
const { merchants, error } = this.state;
const { data } = this.state;
const { order } = this.state;
const getProductsFromMerchant = ({ products, brands }) =>
products.map(({ belongsToBrand, ...product }) => ({
...product,
brand: brands[belongsToBrand]
}));
const products = merchants.reduce(
(acc, merchant) => [...acc, ...getProductsFromMerchant(merchant)],
[]
);
if (loading) {
return (
<Page
pageTitle="Shop"
history
goBack={this.goBack}
goCart={this.goCart}
>
<div className="loading-page">
<i
className="fa fa-spinner fa-spin fa-3x fa-fw"
aria-hidden="true"
/>
<br /> <br />
<span>Loading...</span>
</div>
</Page>
);
}
return (
<Page
pageTitle="Shop"
history
goBack={this.goBack}
goCart={this.goCart}
cartNumber={order.length}
>
<div className="shop-page">
{products.map(({ id, ...product }) =>
<Product
{...product}
key={id}
history
onAddToCart={this.onAddToCart.bind(this)}
/>
)}
</div>
</Page>
);
}
}
export default Shop;
Here is the page component which contains the header component
export const Page = ({
children,
pageTitle,
history,
goBack,
goCart,
cartNumber
}) =>
<div className="page">
<Header goBack={goBack} goCart={goCart} history cartNumber>
{pageTitle}
</Header>
<main>
<MuiThemeProvider>
{children}
</MuiThemeProvider>
</main>
<Footer />
</div>;
export default Page;
And Finally this is the header where I want to bring the cartNumber into.
const Header = ({ children, goBack, goCart, cartNumber, pageTitle }) =>
<header>
<button onClick={goBack} className="back-button">
{/* Image added here to show image inclusion, prefer inline-SVG. */}
<img alt="Back" src={`/icon/header/back-white.svg`} />
</button>
<h1>
{children}
</h1>
<div className="right-content">
( {cartNumber} )
<i
className="fa fa-shopping-cart fa-2x"
aria-hidden="true"
onClick={goCart}
/>
</div>
</header>;
export default withRouter(Header);
You're passing cartNumber as a boolean:
<Header goBack={goBack} goCart={goCart} history cartNumber>
Pass it as a value:
<Header goBack={goBack} goCart={goCart} history={history} cartNumber={cartNumber}>

Resources