Can someone explain why I keep getting an error with the following setup:
I have this statement in my constructor:
this.bitLink = this.bitLink.bind(this),
Then I have my function as follows:
bitLink(url){
let bitly = new Bitly('f06707dhbt4c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
Then I call my function like so:
<p className="shareBtn" onClick={this.bitLink(`/album/${album._id}`)}>Share!</p>
But when I load the page I get this error:
Uncaught TypeError: Cannot read property 'bitLink' of undefined
From what I have researched, this seems to be a proper way to give this the right context but it's still returning undefined.
Edit:
Full component:
import React, { Component } from 'react'
import actions from '../actions'
import { connect } from 'react-redux'
import { APIManager } from '../utils'
import {Router, Route, Redirect, Link, withRouter } from 'react-router-dom'
import {Image, CloudinaryContext, Transformation} from 'cloudinary-react';
import Bitly from 'bitly';
class AlbumBoard extends Component {
constructor(){
super()
this.state = {
albums: []
}
}
render(){
const toPublicId = (image) => {
return image.slice(62, image.length)
}
const bitLink = (url) => {
let bitly = new Bitly('f06707da4944c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
return(
<div className="albumBoard">
{(this.props.currentUser) ?
this.props.currentUser.albums.map(function(album, i){
return <div key={i} className="thumbnailContainer">
<h2>{album.name}</h2>
<Link to={`/album/${album._id}`}>{album._id}</Link>
<p>{album.description}</p>
<div className="albumThumbnailContainer">
<CloudinaryContext cloudName="djswgrool" fetchFormat="auto">
{ (album.images.length < 3) ?
<Image publicId={toPublicId(album.images[0].url)} responsive className="album2">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
:
<div>
<Image publicId={toPublicId(album.images[0].url)} responsive className="album1">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
<Image publicId={toPublicId(album.images[1].url)} responsive className="album2">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
<Image publicId={toPublicId(album.images[2].url)} responsive className="album3">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
</div>
}
</CloudinaryContext>
</div>
<div className="X"></div>
<p className="shareBtn" onClick={bitLink(`/album/${album._id}`)}>Share!</p>
</div>
})
:
null}
</div>
)
}
}
const stateToProps = (state) => {
return {
currentUser: state.account.currentUser
}
}
export default connect(stateToProps)(AlbumBoard)
You are not passing a function reference to the onClick event but the value the returned from the function bitLink.
This is because you are invoking it
onClick={this.bitLink()}
instead of just passing the reference of it
onClick={this.bitLink}
If you want to pass a parameter to it you would need to either:
Create another wrapper function that will return and pass it the parameter:
You can do it in several ways:
currying:
bitLink(url){
return function(e){
let bitly = new Bitly('f06707dhbt4c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
}
or arrow function:
bitLink = (url) => (e){
let bitly = new Bitly('f06707dhbt4c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
or arrow function inside the event:
onClick={() => {this.bitLink(url)}}
Note that this will make a new function instance on each render.
Have you tried like this:
onClink={() => this.bitLink(/album/${album._id})}
Related
I am using next js in my project
when i fetch from my server using useswr hook then there is certain condition to return on the page for example
dashboard.js
import InnerNavbar from "../components/InnerNavbar";
import DashboardLoadingSkeleton from "../components/DashboardLoadingSkeleton";
import DashBoardData from "../components/DashBoardData";
import useSWR, { mutate } from "swr";
const fetcher = async () => await fetch(`${process.env.NEXT_PUBLIC_URL}/fetchchallenges`, {
method: "POST",
credentials: 'include'
}).then((res) => res.json());
const dashboard = () => {
const { data, error } = useSWR("/dashboard", fetcher, { dedupingInterval: 40000 });
if (!data) {
if (!data.Errors) {
return (<><InnerNavbar /><DashBoardData data={data} mutate={mutate} /></>);
} else if (data.Errors === "Not Approved") {
return (<>{location.replace('/notapproved')} <center><h1>Not Approved By Admin</h1></center></>)
}else {
return (<>{location.replace('/logout')} <center><h1>Redirecting...</h1></center></>)
}
} else {
return (
<>
<InnerNavbar />
<DashboardLoadingSkeleton />
</>
)
}
};
export default dashboard;
now i want to need this same conditional statement in multiple pages to return but i don't known how to create a separate function for that.
I think you can use react Higher-Order Components
here is sth that I made:
import useSWR, { mutate } from "swr";
const fetcher = async () => await fetch(`${process.env.NEXT_PUBLIC_URL}/fetchchallenges`, {
method: "POST",
credentials: 'include'
}).then((res) => res.json());
const withSwrData = ({Component,endpoint}) => {
const { data, error } = useSWR(endpoint, fetcher, { dedupingInterval: 40000 });
if (!data) {
if (!data.Errors) {
return (<Component data={data} mutate={mutate} />);
} else if (data.Errors === "Not Approved") {
return (<>{location.replace('/notapproved')} <center><h1>Not Approved By Admin</h1></center></>)
}else {
return (<>{location.replace('/logout')} <center><h1>Redirecting...</h1></center></>)
}
} else {
return (
<>
<Component />
</>
)
}
};
export default withSwrData;
and use it in dashboard in this way :
import InnerNavbar from "../components/InnerNavbar";
import DashboardLoadingSkeleton from "../components/DashboardLoadingSkeleton";
import DashBoardData from "../components/DashBoardData";
import withSwrData from "../withSwrData"
const dashboard = ({data,mutate}) => {
return (<>
<InnerNavbar />
{data ? <DashBoardData data={data} mutate={mutate} /> : <DashboardLoadingSkeleton /> }
</>)
};
export default withSwrData(dashboard,"/dashboard");
I have this component:
import React, { lazy, Suspense } from 'react';
import { ErrorBoundary } from '../ErrorBoundary';
const FALLBACK = <svg aria-label="" data-testid="icon-fallback" viewBox="0 0 21 21" />;
const ERROR = (
<svg data-testid="icon-notdef" viewBox="0 0 21 21">
<path d="M0.5,0.5v20h20v-20H0.5z M9.1,10.5l-6.6,6.6V3.9L9.1,10.5z M3.9,2.5h13.2l-6.6,6.6L3.9,2.5z M10.5,11.9l6.6,6.6H3.9 L10.5,11.9z M11.9,10.5l6.6-6.6v13.2L11.9,10.5z" />
</svg>
);
export const Icon = ({ ariaLabel, ariaHidden, name, size }) => {
const LazyIcon = lazy(() => import(`../../assets/icons/${size}/${name}.svg`));
return (
<i aria-hidden={ ariaHidden }>
<ErrorBoundary fallback={ ERROR }>
<Suspense fallback={ FALLBACK }>
<LazyIcon aria-label={ ariaLabel } data-testid="icon-module" />
</Suspense>
</ErrorBoundary>
</i>
);
};
I’m trying to test the condition where an SVG is passed in that doesn’t exist, in turn rendering the <ErrorBoundary /> fallback. The ErrorBoundary works in the browser, but not in my test.
This is the failing test:
test('shows notdef icon', async () => {
const { getByTestId } = render(<Icon name='doesnt-exist' />);
const iconModule = await waitFor(() => getByTestId('icon-notdef'));
expect(iconModule).toBeInTheDocument();
});
I get this error message:
TestingLibraryElementError: Unable to find an element by: [data-testid="icon-notdef"]”.
How do I access ErrorBoundary fallback UI in my test?
Edit
This is the code for the <ErrorBoundary /> component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
error: '',
errorInfo: '',
hasError: false,
};
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error({ error, errorInfo });
this.setState({ error, errorInfo });
}
render() {
const { children, fallback } = this.props;
const { error, errorInfo, hasError } = this.state;
// If there is an error AND a fallback UI is passed in…
if (hasError && fallback) {
return fallback;
}
// Otherwise if there is an error with no fallback UI…
if (hasError) {
return (
<details className="error-details">
<summary>There was an error.</summary>
<p style={ { margin: '12px 0 0' } }>{error && error.message}</p>
<pre>
<code>
{errorInfo && errorInfo.componentStack.toString()}
</code>
</pre>
</details>
);
}
// Finally, render the children.
return children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
fallback: PropTypes.node,
};
… and this is the full error with DOM that I get for the test:
shows notdef icon
TestingLibraryElementError: Unable to find an element by: [data-testid="icon-notdef"]
<body>
<div>
<i
aria-hidden="false"
class="Icon Icon--sm"
>
<span
aria-label=""
data-testid="icon-module"
/>
</i>
</div>
</body>
<html>
<head />
<body>
<div>
<i
aria-hidden="false"
class="Icon Icon--sm"
>
<span
aria-label=""
data-testid="icon-module"
/>
</i>
</div>
</body>
</html>Error: Unable to find an element by: [data-testid="icon-notdef"]
Lastly, my SVG mock:
import React from 'react';
const SvgrMock = React.forwardRef(
function mySVG(props, ref) {
return <span { ...props } ref={ ref } />;
},
);
export const ReactComponent = SvgrMock;
export default SvgrMock;
As discussed in the comments, it is most likely the mock is avoiding the error. Try re mocking the SVG files with a new mock throwing an error.
// tests that require unmocking svg files
describe('non existent svg', () => {
beforeAll(() => {
jest.mock('.svg', () => {
throw new Error('file not found')
});
});
test('shows notdef icon', async () => {
const { getByTestId } = render(<Icon name='doesnt-exist' />);
const iconModule = await waitFor(() => getByTestId('icon-notdef'));
expect(iconModule).toBeInTheDocument();
});
afterAll(() => jest.unmock('.svg'))
})
It is necessary to wrap it to ensure the SVG files are re-mocked only during the test (beforeAll - afterAll) to not interfere with the rest of the tests.
I creating a comment form which would allow to post a comment below a video. It throws me id error inside the constructor that goes away on page reload. The ownProps comes out as empty object, otherwise I would've used it to get the video id. My solution with window.location.reload() is lame. Does anybody know a better one?
Comment Container
import React from 'react';
import { connect } from 'react-redux';
import Comment from './comment';
import { fetchComments, updateComment, createComment } from '../../util/comment_api_util';
const mSTP = state => {
const id = Object.keys(state.entities.videos);
const video = state.entities.videos[id];
if (video) {
return {
video,
}
} else {
window.location.reload()
}
}
const mDTP = dispatch => {
return {
fetchComments: () => dispatch(fetchComments()),
fetchComment: commentId => dispatch(fetchComment(commentId)),
createComment: comment => dispatch(createComment(comment)),
updateComment: comment => dispatch(updateComment(comment)),
deleteComment: commentId => dispatch(deleteComment(commentId))
}
}
export default connect(mSTP, mDTP)(Comment)
Comment Component
import React from 'react';
class Comment extends React.Component {
constructor(props) {
super(props)
this.state = {
body: "",
// video_id: this.props.video.id,
comment_errors: null,
}
this.update = this.update.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
update() {
return e => this.setState({ body: e.target.value })
}
handleSubmit(e) {
e.preventDefault();
const formData = new FormData();
formData.append('comment[body]', this.state.body);
formData.append('comment[video_id]', this.state.video_id);
$.ajax({
url: '/api/comments',
method: 'POST',
data: formData,
contentType: false,
processData: false
}).then(
(response) => {
this.setState(
{ comment_errors: response.responseJSON },
)
}
).then(() => {
this.setState(
{ body: "", video_id: "", comment_errors: null }
)
});
}
render() {
// if (!this.props.video) return <div />
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
<textarea
type="body"
placeholder="Add a comment"
value={this.state.body}
onChange={this.update()}
className="comment-body"/>
</label>
<button type="submit">Add comment</button>
</form>
</div>
)
}
}
export default Comment;
Play Container
import { connect } from 'react-redux';
import Play from './play';
import { fetchVideo } from '../../actions/video_actions';
import { fetchUsers } from '../../actions/user_actons';
import { fetchComments } from '../../actions/comment_actions';
const mSTP = (state, ownProps) => {
const users = Object.values(state.entities.users)
return {
video: state.entities.videos[ownProps.match.params.id],
users
}
};
const mDTP = dispatch => ({
fetchComments: () => dispatch(fetchComments()),
fetchVideo: videoId => dispatch(fetchVideo(videoId)),
fetchUsers: () => dispatch(fetchUsers()),
});
export default connect(mSTP, mDTP)(Play);
Play Component
import React from 'react';
import Comment from '../comment/comment_container'
class Play extends React.Component {
constructor(props) {
super(props);
}
dateCreated(date) {
const dateCreated = new Date(date)
return dateCreated.toLocaleDateString();
}
componentDidMount() {
this.props.fetchUsers();
this.props.fetchComments();
this.props.fetchVideo(this.props.match.params.id).then(() => {
const video = document.querySelector('.video-player');
video.muted = !video.muted;
video.play()
});
}
render() {
if (!this.props.video) { return null }
console.log(this.props)
const users = this.props.users;
const owner = users.filter(user => user.id === this.props.video.owner_id)[0]
return (
<div id="video-container">
<video
className="video-player"
controls="controls"
src={this.props.video.video_url}
autoPlay="autoplay"
muted
>
</video>
<div id="play-info">
<h1 className="play-title">{this.props.video.video_title}</h1>
<h2 className="play-date">{this.dateCreated(this.props.video.created_at)}</h2>
<h2 className="owner-name">{owner.username}</h2>
<h2 className="play-description">{this.props.video.video_description}</h2>
</div>
<Comment />
<div className="home-footer">
<h2 className="home-footer-1">#2020</h2>
<h2 className="home-footer-2">
Made with
<svg viewBox="0 0 20 20" className="_3Weix"><path d="M10 18a1.23 1.23 0 01-.8-.4 14.25 14.25 0 00-4.4-3.7C2.5 12.3 0 10.7 0 7.5a5.52 5.52 0 011.6-3.9A5.73 5.73 0 016 2a5.25 5.25 0 014 1.9A5.85 5.85 0 0114 2c2.9 0 6 2.2 6 5.5s-2.5 4.8-4.8 6.4a15.51 15.51 0 00-4.4 3.7 1.23 1.23 0 01-.8.4z" fill="rgb(255,0,0)"></path></svg>
NYC
</h2>
</div>
</div>
);
}
}
export default Play;
The only issue here is that the Comment component is trying to render before video loads. I am assuming that you are fetching video which means that if it is not there even for an instant, this error will be thrown.
The solution to this is to conditionally render Comment when you know that video exists. You can do this either in the parent or the child.
Remove this from mapStateToProps:
if (video) {
return {
video,
}
} else {
window.location.reload()
}
And check that video exists before rendering Comment
Possible solution in parent:
{video && video.id ? <Comment video={video} /> : null}
Additionally
You could render some sort of circular progress indicator instead of null
Possible solution in child
Remove video_id from state since this shouldnt be updated anyways and doesn't need to be state. Then check existence before return like this:
if (!video) return <div />
return (
// Your Comment jsx
)
Again you can render some sort of progress indicator instead of div if you want.
Why it works
Whenever video doesn't exist for that instant it will return div and wont crash. Once video exists it will re-render and now will load the Comment component instead of the div. Typically it will happen so quickly that you wont notice.
I'm creating a react application, and I have a component that is define more or less like this:
import React, { Component } from 'react';
import axios from 'axios';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true,
error: null
};
}
componentDidMount() {
var _this = this;
this.serverRequest =
axios
.get("LinkToAPI")
.then(result => {
_this.setState({
data: result.data,
loading: false,
error: null
});
})
.catch(err => {
_this.setState({
loading: false,
error: err
});
});
}
componentWillUnmount() {
this.serverRequest.abort();
}
renderLoading() {
return <div>Loading...</div>
}
renderError() {
return (
<div>
Something when wrong: {this.state.error.message}
</div>
);
}
renderData() {
const { error, data} = this.state;
if (error) {
return this.renderError();
}
return (
<div>
{data.map(d=> {
if (d.imageUrl) {
<div className="dataDiv" style="background: url('{d.imageUrl}')" key={d.Id}>{d.name}</div>
} else {
<div className="dataDiv" style="background: url('LinkToSomeImage')" key={d.Id}>{d.name}</div>
}
}
)}
</div>
)
}
render() {
return (
<div className="App">
{this.props.loading ? this.renderLoading() : this.renderData()}
</div>
);
}
}
It basically gets the JSON data from the API, and using it renders some divs with the data inside the JSON. I'm applying to the divs containing the data dataDiv class, which is define inside my App.css file. Additionally, I want to set a background image for the div. What exactly I want to do is that if the data entry includes a field named imageUrl I want to use that url as a url to the background image, otherwise, if it is null or empty, I want to use a default url that I found from the internet. What is a proper way to handle this in React? The code segment above doesn't seem to work, especially the if-else statement inside the renderData function. How can I fix this code, or is there any way to handle this more gracefully, probably maybe inside the CSS?
I would do like this
Please make sure to check backgroundUrl equal to your desired CSS.
{data.map(d => {
let backgroundUrl = "LinkToSomeImage";
if (d.imageUrl) {
backgroundUrl = d.imageUrl;
}
return (
<div className="dataDiv" style={{backgroundImage: `url(${backgroundUrl})`}} key={d.Id}>{d.name}</div>
)
})}
EDIT
A full function would be:
renderData() {
const { error, data} = this.state;
if (error) {
return this.renderError();
}
return (
<div>
{data.map(d => {
let backgroundUrl = "LinkToSomeImage";
if (d.imageUrl) {
backgroundUrl = d.imageUrl;
}
return (
<div className="dataDiv" style={{backgroundImage: `url(${backgroundUrl})`}} key={d.Id}>{d.name}</div>
)
})}
</div>
)
}
<Box
onClick={() => {
history.push({
pathname: `/p/c/${data.ProductName.replace(/\//g, "~")}/1`
});
}}
css={{
backgroundImage:`url(${data.imageUrl||"/default-placeholder.png"})`,
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}}
>
I know there are many questions regarding this error, I've gone through all that I could find.
I think it may have something to do with a lack of getInitialState but I'm not sure how i'd implement it.
I'm trying to create a dynamic list of text files within a folder, pulling information from the file itself through the frontMatter module.
import React from 'react'
import frontMatter from 'front-matter'
import fs from 'fs'
import { getFileList } from '../util/utils'
export default class DList extends React.Component {
static async getInitialProps () {
return getFileList('./static/filecontainer/').then((files) => {
const linkParams = files.nameList.map((fileName, i) => {
const content = fs.readFileSync(files.pathList[i], 'utf-8')
const meta = frontMatter(content)
return {
name: fileName,
title: meta.attributes.title,
date: meta.attributes.date,
description: meta.attributes.description,
}
})
return {
linkParams: linkParams
}
})
}
render () {
return (
<div id="listContainer">
{
this.props.linkParams.map((el, i) => {
return (
<div className="itemBlock">
<div id="nameBlock"><div id='tFileTitle'>{el.title}</div><div id='tFileDate'>{el.date}</div></div>
<div id="descriptBlock"><p className='tFileInfo'>{el.description}</p></div>
</div>
)
})
}
</div>
)
}
}
Here's the getFileList function:
import yaml from 'js-yaml'
export async function getFileList (dirname) {
return new Promise((resolve, reject) => {
fs.readdir (dirname, (error, files) => {
if (error) {
reject(error);
} else {
files.reverse()
resolve({
pathList: files.map( el => dirname + el ),
nameList: files.map( el => el.slice(0, -3) )
})
}
})
})
}
So the idea is this would create a small box displaying all the information that I could then import into its own div on the main page.
import Header from '../components/Header'
import DList from '../components/DList'
export default () => (
<div>
<Header />
<div id="contentWrap">
<div id="leftContent"><DList /></div>
<div id="rightContent"></div>
</div>
</div>
)
Your DList render function is using this.props.linkParams.map. Your use of DList is just <DList />, it doesn't set any properties at all, so this.props.linkParams is undefined. You need to either pass linkParams as a property when you use DList (<DList linkParams={/*...*/} />), or have render handle it if there is no linkParams property.
I think it may have something to do with a lack of getInitialState
No, state and properties are different things. state is the state of the component, managed by the component. props are properties passed to the component by its user.