Problem
In React, we can pass functions as props into a child component when a function requires access to a state within the parent component of the aforementioned child. I was writing code for an application where I need such behavior to be implemented. However, I'm having trouble finding proper conventions for defining functions in Qwik, and then sending them through.
Attempt
I've tried defining the function within my interface to see if that helps Qwik to allow this implementation but so far that has not worked either.
Code
I'm trying to launch a modal from a icon contained in the header within my application. I'm trying to control displaying the modal by using a store declared within my header component. It's a Boolean value and determines if the modal would be displayed or not. I defined the function for modifying the state within my header component and attempted to pass it into my modal child component.
// components/header/header.tsx
import { component$, useClientEffect$, useStore } from "#builder.io/qwik";
import { strictEqual } from "assert";
import Modal from "../modal/modal";
export default component$(() => {
const store = useStore({
...
modal: false
});
function onClose() {
store.modal = false;
}
return (
<header>
{store.modal && <Modal onClose={onClose}/>}
<div
onClick$={()=>{
store.modal = true;
}}
>
<i class="fa-solid fa-cart-shopping"></i>
</div>
</header>
);
});
Inside my modal component I tried to use an interface to indicate that I'm passing a function into my props and tried to set as the function to execute within another icon contained within my child component.
// components/modal/modal.tsx
import { component$ } from "#builder.io/qwik";
import { strictEqual } from "assert";
interface ModalProps {
onClose: () => void
}
export default component$((props: ModalProps) => {
return (
<div>
<div>
<h1>Modal</h1>
<i onClick$={props.onClose}></i>
</div>
</div>
);
});
Error Message
When I click on the icon within my header, it displays the following error in my terminal.
log.js:10 QWIK ERROR Error: Code(3): Only primitive and object literals can be serialized at Array.flatMap (<anonymous>) ƒ onClose() { store.modal = false; }
Conclusion
Is there anyway to send functions as props into child components in Qwik JS?
If not, can I access stores contained in a parent component from within a child component?
Basically, what would be the ideal approach to solve this issue?
As I'm a noob like you in this framework, I've struggled to understand how this works too.
You actually need to pass a QRL as you may read here:
https://qwik.builder.io/docs/components/events/
So, here's how to modify your code for the Modal component:
import { component$, QRL } from '#builder.io/qwik';
interface ModalProps {
onClose: QRL<() => void>;
}
export default component$<ModalProps>(props => {
return (
<div>
<div>
<h1>Modal</h1>
<i onClick$={props.onClose}></i>
</div>
</div>
);
});
And your head component:
import { $, component$, useStore } from '#builder.io/qwik';
import Modal from '../components/test';
export default component$(() => {
const store = useStore({
modal: false
});
const onClose = $(() => {
store.modal = false;
});
return (
<header>
{store.modal && <Modal onClose={onClose} />}
<div
onClick$={() => {
store.modal = true;
}}
>
<i class="fa-solid fa-cart-shopping"></i>
</div>
</header>
);
});
Related
I am trying to load a component in React via a prop. It is an icon that I want to pass from the parent component.
Dashboard (parent):
import { Button } from './components';
function App() {
return (
<div className="app">
<div className="app__nav">
<Button icon="FiSun" />
<Button icon="FiSun" />
</div>
</div>
);
}
Button (child):
import React from 'react';
import * as Icon from "react-icons/fi";
import './button.scss';
function Button(props) {
return(
<button>
// Something like this
<Icon.props.icon />
</button>
)
}
Unfortunately, I can't find an easy way to make this work since I'm not allowed to use props in the component name.
Here is a working version :
import * as Icons from "react-icons/fi";
function Button(props) {
const Icon = Icons[props.icon];
return <button><Icon/></button>;
}
I added an example on stackblitz
I doubt this is the pattern you want.
If App knows the name of the component Button should render, you really aren't providing any abstraction by not passing the component reference itself. You might be able to get it to work passing the string like this, but I wouldn't recommend going that route.
Instead, I would pass the component reference to Button like this:
import FiSun from '...';
...
<Button icon={FiSun} />
function Button(props) {
const Icon = props.icon; // Alias as uppercase
return(
<button>
<Icon />
</button>
)
}
Or if you want only the Button component to know about the possible icon types, I would suggest using a normal conditional instead of trying to dynamically create the JSX tag:
function Button(props) {
function renderIcon() {
if (props.icon == 'FiSun') {
return <FiSun />;
} // else etc
}
return(
<button>
{renderIcon()}
</button>
)
}
To provide some stability while still keeping the functionality of allowing the component user to pass in any available icon name, you could do something like this:
function Button(props) {
function renderIcon() {
const I = Icon[props.icon];
if (I) {
return <I />;
}
// Icon is not valid, throw error or use fallback.
if (in_development) {
console.error('[Button]: Invalid prop `icon`. Icon '+props.icon+' does not exist.');
}
return <FallbackIcon />
}
return(
<button>
{renderIcon()}
</button>
)
}
I am trying to create a dynamically updating navbar in SvelteKit, with the currently open section formatted accordingly. I am attempting to identify the page based on the first part of the path, as below:
__layout.svelte:
<script context="module">
export const load = ({ page }) => {
return {
props: {
currentSection: `${page.path}`.split('/')[0],
sections: ['home', 'dashboard', 'settings']
}
};
}
</script>
<div class="min-h-screen bg-gray-100">
<Header {...props} />
<slot />
</div>
Header.svelte
<script>
import Menu from "$lib/nav/menu.svelte"
</script>
<Menu {...props}></Menu>
Menu.svelte
<script>
export let sections;
export let currentSection;
</script>
{#each sections as { section }}
<a
href="/{section}"
class="{section == currentSection
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700'} other-classes"
>{section}</a
>
{/each}
This is resulting in a props is not defined error, but I would have expected props to be defined since I've defined it in the return from the load() fundtion on the primary layout (based on the docs).
Do I somehow need to explicitly declare the props rather than expecting them to be available from the return of the load() function?
The props are passed from the module script to the regular component script, this means you still need to add export let props in your layout as well.
<script context="module">
export const load = () => {
return {
props: {
test: 123
}
}
}
</script>
<script>
export let test; //
</script>
Note that this will always spread out the props, you cannot do export let props and retrieve all the props, you can however get all the props passed to a component using $$props
Also, the load function is only available for pages and layouts, so you will for sure have to export props in both Header and Menu as those are just regular svelte components.
My application consists of several pages. Every page has a ToastContainer component and some other component that behaves as a small single-page application (in this case, Job):
Note that Job and ToastContainer are siblings.
I have set up some basic toasts in my application and I want to be able to call a method on ToastContainer called pushToast(...) from anywhere in my application, since many child components of make AJAX calls that return feedback/responses to the user and it is not feasible to pass down a toast method into every component that I have.
const ToastContext = React.createContext(); //???
export default class ToastContainer extends Component {
constructor(props) {
super(props);
this.state = {
toastList: [],
}
}
render() {
return(
<div id="toast-container" className="toast-container position-absolute top-0 end-0 p-3" style={{'zIndex': 999}}>
{this.state.toastList.map(toast => (
<Toast .../>
))}
</div>
)
}
pushToast = (title, time, content) => //HOW CAN I MAKE THIS METHOD ACCESSIBLE TO JOB AND ITS CHILDREN?
{
var newToast = {
title: title,
time: time,
content: content,
}
this.setState({
toastList: [...this.state.toastList, newToast]
})
}
I think what I need to use are React.js contexts, but I don't know where to define the context and if the other components (such as Job) will have access to it. I need to somehow send pushToast defined in ToastContainer into every component (globally) so that I can call it from anywhere I want
way to solve this problem could be creating a context wrapper, first of all, you have to create a file that represents the ToastContextWrapper then creates a wrapper component that holds the state of your toats and pushToast function and then passes it to the context provider and wraps whole your project (because you want to access it from everywhere you want, otherwise you can consider a specific scope).
let me give you an example by code:
ToastContextWrapper.jsx:
export const ToastContext = React.createContext(undefined);
export default function ToastContextWrapper({ children }) {
const [toastData, setToastData] = React.useState([]);
const pushToast = (newToast) => setToastData((prev) => [...prev, newToast]);
return (
<ToastContext.Provider
value={{
value: toastData,
pushToast,
}}>
{children}
</ToastContext.Provider>
);
}
then you have to wrap your project by this component:
index/App.js:
const Child = () => {
const toastContext = React.useContext(ToastContext);
return (
<>
<h1>{JSON.stringify(toastContext.value)}</h1>
<button onClick={() => toastContext.pushToast('BlahBlah ')}>
Call ToastData!
</button>
</>
);
};
function App() {
return (
<ToastContextWrapper>
<Child />
</ToastContextWrapper>
);
}
in this way by using ToastContext you have access to the values, the first one is toast state, and the seconds one is pushToast.
I'm trying to build a quiz that uses react-modal to provides hints. I will need multiple modals inside the quiz. I'm new to React so it's quite possible that I'm making a simple mistake.
I'm not sure it matters, but I've built this using create-react-app.
My App.js looks like this:
import React, { Component } from 'react';
import HintModal from './hintModal';
import Modal from 'react-modal';
import './App.css';
Modal.setAppElement('#root');
class App extends Component {
state = {
modalIsOpen: false,
hint: ''
};
openModal = (hint) => {
this.setState({ modalIsOpen: true, hint: hint });
}
closeModal = () => {
this.setState({ modalIsOpen: false, hint: '' });
}
render() {
return (
<React.Fragment>
<h1>Modal Test</h1>
<h2>First Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="mango"
/>
<hr />
<h2>Second Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="banana"
/>
</React.Fragment>
);
}
}
export default App;
hintModal.jsx looks like this:
import React, { Component } from 'react';
import Modal from 'react-modal';
const HintModal = (props) => {
const {openModal, modalIsOpen, closeModal, hint} = props;
return (
<React.Fragment>
<button onClick={ () => openModal(hint) }>Open Modal</button>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Example Modal"
>
<h2>Hint</h2>
<p>{hint}</p>
<button onClick={closeModal}>Close</button>
</Modal>
<p>We should see: {hint}</p>
</React.Fragment>
);
};
export default HintModal;
Here's the problem: I need the content of the modal to change based on the hint prop passed to HintModal. When I output hint from outside <Modal>, it behaves as expected, displaying the value of the prop. But when I output hint within <Modal>, it returns "banana" (the value of the hint prop for the second instance of HintModal) when either modal is activated.
Any help would be greatly appreciated!
You are controlling all of your modals with the same piece of state and the same functions to open and close the modal.
You need to either have just one modal and then dynamically render the message inside it or you need to store a modalIsOpen variable in your state for every single modal.
I have a modal component with two methods that show/hide the modal. How can I call those methods from another component?
This is the code for the Modal:
// Dependencies
//==============================================================================
import React from 'react'
import Modal from 'boron/DropModal'
// Class definition
//==============================================================================
export default class RegistrationModal extends React.Component {
showRegistrationModal() {
this.refs.registrationModal.show()
}
hideRegistrationModal() {
this.refs.registrationModal.hide()
}
render() {
return (
<Modal ref="registrationModal" className="modal">
<h2>Meld je aan</h2>
<button onClick={this.hideRegistrationModal.bind(this)}>Close</button>
</Modal>
)
}
}
You can call a components method from the outside as long as you keep a reference to the component. For example:
let myRegistrationModal = ReactDOM.render(<RegistrationModal />, approot );
// now you can call the method:
myRegistrationModal.showRegistrationModal()
It's a bit cleaner if you pass a reference to the modal to another component, like a button:
let OpenModalButton = props => (
<button onClick={ props.modal.showRegistrationModal }>{ props.children }</button>
);
let myRegistrationModal = ReactDOM.render(<RegistrationModal />, modalContainer );
ReactDOM.render(<OpenModalButton modal={ myRegistrationModal }>Click to open</OpenModalButton>, buttonContainer );
Working example: http://jsfiddle.net/69z2wepo/48169/
You cant call it from another component, because its a method belong to RegistrationModal component, but you can refactor your code so you can call it
export function hideRegistrationModal() {
console.log("ok");
}
export default class RegistrationModal extends React.Component {
render() {
return (
<Modal ref="registrationModal" className="modal">
<h2>Meld je aan</h2>
<button onClick={hideRegistrationModal}>Close</button>
</Modal>
)
}
}
now you can call from anywhere but you need to import it first like this
import { RegistrationModal, hideRegistrationModal } from 'path to modal.js'
// ^-- Component name ^-- Method
What you want to do is create a parent component which will handle the communication between your modals.
A really great example and explanation can be found here: ReactJS Two components communicating
This is a good approach because it keeps your components decoupled.