I am creating my first ever self made web development project and have run into an infinite loop. The full project can be found on https://github.com/Olks95/my-dnd/tree/spellbook. So the question is: What causes the loop and how do I fix it?
(The loop happens somewhere in the 2nd item of the 'Playground' component when the ContentSelector - Spellbook is called. The custom hook useHandbook is called in Spellbook and continously calls the API, should obviously only happen once... refresh or click return to stop spamming )
From what I can tell the issue is not in the custom hook itself, as I have made several attempts to rewrite it and an empty dependency array is added to the end of the useEffect(). I will try to explain with example code here.
import { Component1, Component2, Component3 } from './ContentSelector.js';
const components = {
option1: Component1,
option2: Component2
option3: Component3
}
const Playground = (props) => {
const LeftItem = components['option1']
const MiddleItem = components['option2']
const RightItem = components['option3']
...
}
I wanted to be able to choose what content to put in each element and ended up making a ContentSelector component that has all the content components in one file, and individually imported/exported. This seems like a strange way to do it, but it was the only way I found to make it work. (Maybe the cause of the loop?) Since this is still fairly early on in the development the selection is hard coded. The item variables starts with a capital letter so I can later call them as components to render like so:
<LeftItem ...some properties... />
Playground then returns the following to be rendered:
return(
<React.Fragment>
<div className="container">
<div className="flex-item">
/* Working select-option to pass correct props to Component1 */
<div className="content">
<LeftItem ...some properties... />
</div>
</div
<div className="flex-item">
/* Currently the same selector that changes the content of the LeftItem */
<div className="content">
<MiddleItem ...some properties... />
</div>
</div>
/*RightItem follows the same formula but currently only renders "coming soon..." */
</div>
</React.Fragment>
)
The Content selector then has the three components where:
Component1: calls a custom hook that only runs once. The information is then sent to another component to render. All working fine.
Component2: calls a custom hook infinite times, but is expected to work the same way component 1 does...
Component3: Renders coming soon...
See Component1 and 2 below:
export const Component1 = (props) => {
const [ isLoading, fetchedData ] = useDicecloud(props.selectedChar);
let loadedCharacter = null;
if(fetchedData) {
loadedCharacter = {
name: fetchedData[0].Name,
alignment: fetchedData[0].Alignment,
/* a few more assignments */
};
}
let content = <p>Loading characters...</p>;
if(!isLoading && fetchedData && fetchedData.length > 0) {
content = (
<React.Fragment>
<Character
name={loadedCharacter.name}
alignment={loadedCharacter.alignment}
/* a few more props */ />
</React.Fragment>
)
}
return content;
}
export const Component2 = (props) => {
const [ fetchedData, error, isLoading ] = useHandbook('https://cors-anywhere.herokuapp.com/http://dnd5eapi.co/api/spells/?name=Aid')
let content = <p>Loading spells...</p>;
if(!isLoading && fetchedData) {
/* useHandbook used to return text to user in a string in some situations */
if(typeof fetchedData === 'string') {
content = (
<React.Fragment>
<p> {fetchedData} </p>
</React.Fragment>
)
} else {
content = (
<React.Fragment>
<Spellbook
/* the component that will in the future render the data from the API called in useHandbook */
/>
</React.Fragment>
)
}
}
return content;
}
I have been working on this issue for a few days and it is getting more confusing as I go along. I expected the mistake to be in useHandbook, but after many remakes it does not seem to be. The current useHandbook is very simple as shown below.
export const useHandbook = (url) => {
const [ isLoading, setIsLoading ] = useState(false);
const [ error, setError ] = useState(null);
const [ data, setData ] = useState(null);
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(url, {
method: "GET",
mode: 'cors'
});
const json = await res.json();
setData(json);
setIsLoading(false);
} catch(error) {
setError(error);
}
};
useEffect(() => {
fetchData();
}, []); //From what the documentation says, this [] should stop it from running more than once.
return [ data, error, isLoading ];
};
EDIT: I ran the chrome developer tools with the react extension and saw something that might be useful:
Image showing Component2 (Spellbook) run inside itself infinite times
You can modify your custom hook useHandbook's useEffect hook to have URL as dependency, since useEffect is similar to componentWillMount, componentDidUpdate and componentWillUnmount, in your case it is componentDidUpdate multiple times. So what you can do is.
useEffect(() => {
fetchData();
}, [url]);
Since there is no need to fetch data agin unless URL is changed
I found the mistake. Component2's real name was Spellbook which I had also called the rendering component that I had not yet made. It turns out I was calling the component from within itself.
Easy to see in the image at the edit part of the question.
Related
I am writing a React editor component, it could be used to write new posts or update an existing post.If used as an update editor, the props MUST receive title and content; if used as new post editor, title and content will not exist.
But I don't know how to let typescript access the "flag" props so that it could decide which interface it could use.
I want to use the component as follows:
<PostEditor flag="newPost"/>
<PostEditor
flag="updatePost"
title="a good story"
content="once upone a time"
/>
I write the interface as follows:
interface NewPostProps{
flag:"newPost",
}
interface UpdatePostProps{
flag:"updatePost",
title:string,
content:string,
}
type IPostEditorProps<T>= T extends "newPost"?NewPostProps:UpdatePostProps
I write the react component as this. It does not work. I want the flag props to be the generic type but I don't know how to write that.
export const PostEditor=({flag,title}:IPostEditorProps<T>)=>{
// component contents
return (<></>)
}
Thank you for your help.
Working example code:
interface CommonProps {
children?: React.ReactNode;
// ...other props that always exist
}
interface NewPostProps {
content: never;
flag: "newPost";
title: never;
}
interface UpdatePostProps {
content: string;
flag: "updatePost";
title: string;
}
type ConditionalProps = NewPostProps | UpdatePostProps;
type Props = CommonProps & ConditionalProps;
export const PostEditor = (props: Props): JSX.Element => {
const { children, content, flag, title } = props;
if (flag === "newPost") return <>{/* implement newpost */}</>;
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
{children}
</div>
);
};
Useful link: conditional react props with typescript
If you have any questions, feel free to ask!
I have a blog that is using getStaticProps + getStaticPaths to generate pages from headless WP - all good. Then I have a component that is connecting to the database to allow me to link to the next post in any within any individual category. This component is then used inside the ...postSlug page.
So basically the component works perfectly if I switch to using getServerSideProps for posts, as it updates the 'previous' + 'next' URLs as each post is requested and built on the server. However, if I use getStaticProps the component doesn't update with the correct URLs unless I manually refresh the page. So I need to use some client-side data fetching within the component, with useState, useEffect or SWR or something - but I'm not sure about the best way to do it (or even if that would work or if I should just use gServerSideProps).
Any help would be greatly appreciated. Thanks! Component below edited for clarity ...
export default function MovePost() {
const { usePost } = client
const post = usePost()
const { query = {} } = useRouter()
const { categorySlug } = query
const currentPaginationCursor = btoa( `arrayconnection:${post.databaseId}` )
const { posts } = client.useQuery()
const previous = posts({
first: 1,
before: currentPaginationCursor,
where: { categoryName: `${categorySlug}` },
}).nodes[0]?.slug
return (
<>
{ previous &&
<Link href={`/archive/${categorySlug}/${previous}`}>
<a ...
export default function Page() {
const { usePost } = client
const post = usePost()
return (
<>
<Layout>
<div dangerouslySetInnerHTML={{ __html: post?.content() ?? '' }} />
<MovePost />
</Layout>
</>
)
}
export async function getStaticProps(context: GetStaticPropsContext) {
return getNextStaticProps(context, {
Page,
client,
notFound: await is404(context, { client }),
})
}
export function getStaticPaths() {
return {
paths: [],
fallback: 'blocking',
}
}
I'm working with next.js and firebase.
I'm doing authentication with google and with phone number.
The problem I have happens when authenticating with phone number, when the recaptcha is rendered, I can't click it and move forward to the next steps in the auth flow, because a transparent container also is rendered and overlaps completly ui.
I don't know why this happens, I followed every step of the google guide:
Phone authentication
My page component is the following...
In the page component I'm rendering a different form, one if it's time to write the verification code sent via SMS and another which is the first in being showed, to write and send the phone number.
import AppHead from '../../components/head'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from "react-i18next"
import Select from '../../components/select'
import Nav from '../../components/nav'
import PhoneNumberInput from '../../components/phone-input'
import Dialog from '../../components/dialog'
import Footer from '../../components/footer'
import { isPossiblePhoneNumber } from 'react-phone-number-input'
export default function Home({ M, authService, libService }) {
const verificationCodeInput = useRef(null)
const [phoneNumber, setPhoneNumber] = useState("")
const [shouldVerify, setShouldVerify] = useState(false)
const [openDialog, setOpenDialog] = useState(false)
const [valid, setValid] = useState(2)
const [confirmationCode, setConfirmationCode] = useState('')
const { t, i18n } = useTranslation('common')
let verificationValidClass = ""
const onChangePhoneNumber = (value) => {
setPhoneNumber(value)
};
const onClickLoginWithPhone = async (evt) => {
evt.preventDefault()
try {
if (isPossiblePhoneNumber(phoneNumber)) {
const confirmationResult = await authService.signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier)
window.confirmationResult = confirmationResult
setShouldVerify(true)
} else {
setOpenDialog(true)
}
} catch (error) {
console.log(error)
}
};
const phoneNumberForm = (
<form className="home__formLogin">
<PhoneNumberInput onChangePhoneNumber={onChangePhoneNumber} phoneNumber={phoneNumber} labelText={t("forms.auth.phoneFieldLabel")} />
<button data-target="phoneLoginModal" className="btn-large home__login-phone modal-trigger" onClick={onClickLoginWithPhone}>
<span className="material-icons home__sendIcon" aria-hidden="true">
send
</span>
{t("forms.auth.sendPhone")}
</button>
<div id="recaptcha-container">
</div>
</form>
)
if (valid === 0) {
verificationValidClass = "invalid"
} else if (valid === 1) {
verificationValidClass = "valid"
}
useEffect(() => {
const elems = document.querySelectorAll('.modal');
const instances = M.Modal.init(elems);
window.recaptchaVerifier = authService.getVerifier('recaptcha-container', (response) => setShouldVerify(true), () => console.log("captcha-expired"))
}, []);
return (
<>
<main className="main">
<div className="main__layout home">
<Nav top={true} content={pickLanguage} contentLg={pickLanguageLG} />
<AppHead title={t("pages.login_phone.meta-title")} description={t("metaDescription")} />
<header className="home__header">
<h1>{shouldVerify ? t("pages.home.titleCode") : "LOGIN"}</h1>
<p>{t("pages.login_phone.subtitle_1")} <br /> {t("pages.login_phone.subtitle_2")}</p>
</header>
<section className="home__login-options">
<h1 className="home__formTitle">{shouldVerify ? t("pages.home.code") : t("pages.home.loginHeading")}</h1>
{shouldVerify ? verificationCodeForm : phoneNumberForm}
<Dialog open={openDialog} onConfirm={onClickConfirmDialog} heading={t("components.dialog.wrongPhoneNumber")} confirmText={t("components.dialog.wrongPhoneNumberConfirm")}>
{t("components.dialog.wrongPhoneNumberContent")}
</Dialog>
</section>
</div>
</main>
<Footer />
</>
)
}
In the useEffect callback I create the verifier object and then in onClickLoginWithPhone I submit the phone number.
The function call authService.signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier) is implemented as shown below:
export default function AuthService(authProvider,firebase,firebaseApp) {
return Object.freeze({
signInWithPhoneNumber,
getVerifier,
getCredentials
})
function getVerifier(id,solvedCallback,expiredCallback){
return new firebase.auth.RecaptchaVerifier("recaptcha-container",{
'size': 'normal',
'callback': solvedCallback,
'expired-callback': expiredCallback
});
}
//used when user signs in with phone number and need to introduce a verification code sended via SMS
async function getCredentials(verificationId,verificationCode){
const credential = firebase.auth.PhoneAuthProvider.credential(verificationId,verificationCode);
const userCredential = await firebase.auth(firebaseApp).signInWithCredential(credential);
return userCredential
}
async function signInWithPhoneNumber(phoneNumber, phoneVerifier) {
const confirmationResult = await firebase.auth(firebaseApp).signInWithPhoneNumber(`${phoneNumber}`, phoneVerifier)
return confirmationResult
}
}
The container that is displayed is the following:
The part highlighted in blue is the full element which appears when I submit the phone number, and the part highlighted in red is the div which spans all the viewport and dont let me click the captcha widget
I really don't know why this thing is displayed, I'm not manipulating the DOM when submit the phone number, so I guess this problem is related to using next with firebase or with firebase itself.
Note: t("<json-object-field>"), t is just a function which reads an specific json file (a different one depending on the language the page is being showed) which contains all the strings translated to an specific language and this function returns a translated string individually.
The captcha widget is rendered but the container overlaps it, the container is transparent.
I have been working on a React web application with a dynamic theme using the emotion-theming library. So a user can switch between environments and each environment has its own theme.
I have created my own CustomThemeProvider which I use to dynamicly change the theme. Below is the code.
export interface CustomThemeContextValue {
customTheme?: Theme;
setCustomTheme: (theme: Theme) => void;
};
const CustomThemeContext = React.createContext<CustomThemeContextValue>({
customTheme: undefined,
setCustomTheme: (theme) => { }
});
interface CustomThemeProviderProps {
}
export const CustomThemeProvider: FC<CustomThemeProviderProps> = (props) => {
const [customTheme, setCustomTheme] = useState<Theme>(theme);
const context: CustomThemeContextValue = React.useMemo(() => ({
customTheme,
setCustomTheme
}), [customTheme, setCustomTheme]);
return (
<CustomThemeContext.Provider value={context}>
<ThemeProvider theme={customTheme} {...props} />
</CustomThemeContext.Provider>
);
};
export const useCustomTheme = () => {
const context = React.useContext(CustomThemeContext);
if (!context) {
throw new Error('useCustomTheme must be used within a CustomThemeProvider');
}
return context;
};
The provider is implemented in the root like so
const Root = () => {
return (
<StrictMode>
<CustomThemeProvider>
<Normalize />
<Global styles={globalStyle} />
<App />
</CustomThemeProvider>
</StrictMode>
);
};
So this code is working, I can get the theme within a function component using the emotion useTheme hook like below:
const theme: Theme = useTheme();
But the question is how to get the theme out of the emotion ThemeProvider and use it in certain situations. Is it possibe to use it in a context like
export const style: Interpolation = {
cssProp: value
};
Or is it usable in a context like below where styled.button is from emotion/styled.
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps> = styled.button([]);
and is it usable in the emotion/core method css() like below
const style = css({
cssProp: value
});
I find it very hard to find answers to these questions using google so I hope somebody here can help me out.
So after a while I have finally found an answer to my own question and I like to share it with everyone as it is very hard to find. so here it is.
Instead of Interpolation you can use InterpolationWithTheme like below:
import { InterpolationWithTheme } from '#emotion/core';
export const style: InterpolationWithTheme<Theme> = (theme) => ({
cssProp: theme.value
});
This way you can get the theme out of the ThemeProvider.
With the styled componenents you can implement it like below:
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps>
= styled.button(({ theme }: any) => ([
{
cssProp: theme.value
}
]);
And finally when you want to use the css() with the themeProvider you have to replace it with the InterpolationWithTheme to make it work just like in the first example of this answer.
These answers have been found by a combination of looking in the emotionjs docs and inspecting the emotionjs types/interfaces.
Cliffs:
I'm trying to pre-fill forms from a user's previous entries, which is all stored in a MongoDB colelction I do this doing conventional Javascript:
componentDidMount(){
let name = document.getElementById("name");
name.value = this.props.iData.name;
}
This works fine unless I refresh the page, in which case I get an error that this.props.iData is undefined. So, whenever I visit the page with the pre-filled data, it works fine and subscription works fine. But when I refresh that same page, the subscription doesn't load fast enough.
The subscription is done like this:
export default createContainer(function(){
const subHandle = Meteor.subscribe("iData", function(){
});
const isLoading = !subHandle.ready();
return {
isLoading,
iData: Poll.find().fetch(),
}
}, UserSettings)
I must be doing something wrong for this to happen the way it's happening.
Did you make sure that your data is ready when componentDidMount is getting called. And also you shouldnt use document.getElementById() in react.
In react you need to use refs.
class Sample extends React.Component {
componentDidMount() {
this.ref.input.value = 'some value';
}
render() {
return (
<input ref="input" />
);
}
}
You can also do this:
class Sample extends React.Component {
render() {
return (
<div>
{!this.props.isLoading ? (
<input value = {this.props.someValue}/>
) : (
<p>Loading </p>
)}
</div>
);
}
}
<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>