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>
)
}
Related
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>
);
});
I'm dealing with a problem passing a prop to a parent component from it's child.
The idea of the code that I'm trying to make work is a set of buttons in a header component that when clicked, load new component pages for other parts of the website. I'm dealing with a couple of smaller bugs that I can fix at another time but the primary issue is when I try to pass the results of the function for handling the switch and the values showing as 'undefined' once they get to the App component. I doubt I'm explaining it well so allow me to show the code.
Parent Component (App)
import React from "react";
import Header from "./Components/Header/Header";
import Footer from "./Components/Footer/Footer";
import Pane from "./Components/Pane/Pane";
import MainPane from "./Components/Pane/MainPane";
import BookViewPane from "./Components/Pane/BookViewPane";
import AddBookPane from "./Components/Pane/AddBookPane";
import SignInPane from "./Components/Pane/SignInPane";
import "./App.css";
const App = ()=>{
function LoadPaneHandler(props){
var NewPaneName=props.paneName;
// const NewPaneName={
// name: props.paneName
// };
// const NewPaneName=String(props);
console.log(NewPaneName);
switch(NewPaneName){
case 'MainPane':
return <MainPane />
case 'AddBookPane':
return <AddBookPane />
case 'BookViewPane':
return <BookViewPane />
case 'SignInPane':
return <SignInPane />
default:
return <Pane />
}
}
return(
<React.Fragment>
<Header switchPane={LoadPaneHandler} />
<main>
<LoadPaneHandler paneName="MainPane" />
</main>
<Footer />
</React.Fragment>
);
}
export default App;
Child Component (Header)
import React from "react";
import "./Header.css";
const Header=(props)=>{
var paneName="";
const switchPaneHandler=event=>{
event.preventDefault();
console.log(paneName);
props.switchPane(paneName);
}
return(
<header id="header">
<div id="header-title">
<h1>Library</h1>
</div>
<div id="header-buttons">
<button onClick={paneName="BookViewPane",switchPaneHandler}> View Books</button>
<button onClick={paneName="AddBookPane",switchPaneHandler}> Add Books </button>
<button onClick={paneName="SignInPane",switchPaneHandler}> Login</button>
</div>
</header>
);
}
export default Header;
I've included the commented out code of other approaches I've used to get the data I need for the function to work properly so that you can have an Idea of what I've already tried.
The code works fine so long as I only pass values to the function from within the App component. Whenever I click on one of the buttons in the header though, it shows the 'paneName' correctly in the 'switchPaneHandler' function but then in 'LoadPaneHandler' it prints as 'undefined'.
I'm still quite new to React so it's likely a very obvious mistake that I've made but any help is appreciated all the same. Thanks!
I think the key issue here is probably caused by confusion about "What are props? What is state?" - very common when getting started with React.
If we look at the parent component first, you're passing LoadPaneHandler to your Header like it's a callback function. That's not how we do it in React. We need to supply a callback function that takes the name of the pane that we want the parent to show. Naming can really help too in order to make things clearer. Here's how I'd rewrite your parent:
const App = ()=>{
const [currentPaneName, setCurrentPaneName] = React.useState("MainPane")
function updateCurrentPane(newPaneName) {
console.log(`Updating pane from ${currentPaneName} to ${newPaneName}`);
setCurrentPaneName(newPaneName)
}
function LoadPaneHandler(){
console.log(`Showing pane ${currentPaneName}`);
switch(currentPaneName){
case 'MainPane':
return <MainPane />
case 'AddBookPane':
return <AddBookPane />
case 'BookViewPane':
return <BookViewPane />
case 'SignInPane':
return <SignInPane />
default:
return <Pane />
}
}
return(
<React.Fragment>
<Header onNewPaneSelected={updateCurrentPane} />
<main>
<LoadPaneHandler />
</main>
<Footer />
</React.Fragment>
);
}
I've left a sprinkling of console.logs in there so you can get a feel for the timing of re-rendering and responding (React-ing) to change. If you're not familiar with useState(), this is the one React hook you absolutely must understand if you're going to build useful React applications - here are the docs.
Now for the child. You've got a paneName variable but you don't need it. All you're really trying to do is set up the right argument when you call the callback:
const Header=(props)=>{
const switchPaneHandler= (paneName) =>{
event.preventDefault();
console.log(paneName);
props. onNewPaneSelected(paneName);
}
return(
<header id="header">
<div id="header-title">
<h1>Library</h1>
</div>
<div id="header-buttons">
<button onClick={() => switchPaneHandler("BookViewPane")}> View Books</button>
<button onClick={() => switchPaneHandler("AddBookPane")}> Add Books </button>
<button onClick={() => switchPaneHandler("SignInPane")}> Login</button>
</div>
</header>
);
}
Note I changed the name of the callback function it's expecting to be given as a prop - switchPane was a bit misleading - it's not this component's job to switch panes, it just needs to be able to tell someone that the user wants to switch. This also makes it easier to make changes in the future, for example if you have other things that are interested in which pane the user wants to see.
Try to change your child component (Header) like this.
import React from "react";
import "./Header.css";
const Header = (props) => {
const switchPaneHandler = paneName => {
console.log(paneName);
props.switchPane(paneName);
}
return(
<header id="header">
<div id="header-title">
<h1>Library</h1>
</div>
<div id="header-buttons">
<button onClick={()=> switchPaneHandler('BookViewPane')}> View Books</button>
<button onClick={() => switchPaneHandler("AddBookPane")}> Add Books </button>
<button onClick={() => switchPaneHandler("SignInPane")}> Login</button>
</div>
</header>
);
}
In header component your passing the paneName directly, but in parent component you are trying to get as javascript object (props.paneName), that is why you are getting error.
const switchPaneHandler=event=>{
event.preventDefault();
console.log(paneName);
props.switchPane(paneName);
}
so try to access directly,
function LoadPaneHandler(paneName){
var NewPaneName = paneName;
console.log(NewPaneName);
switch(NewPaneName){
case 'MainPane':
return <MainPane />
case 'AddBookPane':
return <AddBookPane />
case 'BookViewPane':
return <BookViewPane />
case 'SignInPane':
return <SignInPane />
default:
return <Pane />
}
}
I am trying to optimize a component of mine with dynamic rendering, but I'm facing some issues, so this is my current situation:
I have an Alert component that renders an alert message, along with an icon.
I have a Icons module, which is a library of icons
I am currently rendering the icon as follows (this is not actual code, it's just to give the idea):
import * as icons from '../icons';
import DefaultIcon from '../icons';
function Alert(iconName='defaultIcon', message) {
function getIcon(iconName) {
if (iconName === 'defaultIcon') {
return DefaultIcon()
}
return icons[iconName]
}
return (
<div>
<span>{getIcon(iconName)}</span>
<span>{message}</span>
</div>
)
}
So, suppose Alert gets called without iconName most of the times, so the components doesn't need to import all of the icons at all.
I would like to avoid including all of the icons in the Alert component by default, but only doing so if iconName is specified
Is it even possible to do so?
I don't think it's possible this way.
Maybe create a component for the Icon that imports the icon libary. In the Alert component you could implement the Icon component as a child:
<Alert message="Alert!">
<Icon iconName="defaultIcon" />
</Alert>
You should import icons dynamically with type or name etc. something like below.
import React from 'react';
export default function Icon({ type, ...props }) {
const Icon = require(`./icons/${type}`).default;
return <Icon {...props} />
}
import Icon from './Icon';
<Icon type="addIcon" />
Ok, looks like I managed, and that's how I did it:
import DefaultIcon from '../icons';
function Alert(message, iconName="") {
const [icon, useIcon] = useState();
//componentDidMount
const useEffect(() => {
//if an icon name is specified, import the icons
if (iconName) {
import("./icons").then(icons => setIcon(icons[iconName]))
} else {
setIcon(DefaultIcon)
}
}
,[])
return (
<span>{icon}</span>
<span>{message}</span>
)
}
I have a list of react-icons passed through props in a "Card Component". In my Card component's render method, I have something like this:
render() {
...
{
this.props.icons.map(icon => {
return (
<div className="icon-square">
/* What should I do here so I render the "icon" variable" */
</div>
)
})
}
}
Note: The list consists of react-icons which are React components themselves.
I tried a lot of things, but I can't quite figure out how I can render the icon. It would be awesome if someone could help me. Thank you
Let say you've passed a list of an icon like
import { FaBeer, FaBug, FaAnchor, FaCoffee } from 'react-icons/fa';
const icons = [FaBeer, FaBug, FaAnchor, FaCoffee];
ReactDOM.render(
<CardComponent icons = {icons} />,
document.querySelector('root')
};
CardComponent.js
class CardComponent extends React.Component{
...
render() {
// Icon is a Component
return (
this.props.icons.map((Icon) => {
return <Icon />
});
)
}
}
If the icon is a react component, then:
this.props.icons.map(Icon => {
return (
<div className="icon-square">
<Icon/>
</div>
)
})
Here is two difference for use your icon, If you pass as a JSX you should use {icon}
But if you pass as a component you should use like this <Icon/>
I think wrapping you need to just put icon is {}
render() {
...
{
this.props.icons.map(icon => {
return (
<div className="icon-square">
{icon}
</div>
)
})
}
}
I have the following (using Material UI)....
import React from "react";
import { NavLink } from "react-router-dom";
import Tabs from "#material-ui/core/Tabs";
import Tab from "#material-ui/core/Tab";
function LinkTab(link){
return <Tab component={NavLink}
to={link.link}
label={link.label}
value={link.link}
key={link.link}
/>;
}
In the new versions this causes the following warning...
Warning: Function components cannot be given refs. Attempts to access
this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of ForwardRef.
in NavLink (created by ForwardRef)
I tried changing to...
function LinkTab(link){
// See https://material-ui.com/guides/composition/#caveat-with-refs
const MyLink = React.forwardRef((props, ref) => <NavLink {...props} ref={ref} />);
return <Tab component={MyLink}
to={link.link}
label={link.label}
value={link.link}
key={link.link}
/>;
}
But I still get the warning. How do I resolve this issue?
Just give it as innerRef,
// Client.js
<Input innerRef={inputRef} />
Use it as ref.
// Input.js
const Input = ({ innerRef }) => {
return (
<div>
<input ref={innerRef} />
</div>
)
}
NavLink from react-router is a function component that is a specialized version of Link which exposes a innerRef prop for that purpose.
// required for react-router-dom < 6.0.0
// see https://github.com/ReactTraining/react-router/issues/6056#issuecomment-435524678
const MyLink = React.forwardRef((props, ref) => <NavLink innerRef={ref} {...props} />);
You could've also searched our docs for react-router which leads you to https://mui.com/getting-started/faq/#how-do-i-use-react-router which links to https://mui.com/components/buttons/#third-party-routing-library. The last link provides a working example and also explains how this will likely change in react-router v6
You can use refs instead of ref. This only works as it avoids the special prop name ref.
<InputText
label="Phone Number"
name="phoneNumber"
refs={register({ required: true })}
error={errors.phoneNumber ? true : false}
icon={MailIcon}
/>
In our case, we were was passing an SVG component (Site's Logo) directly to NextJS's Link Component which was a bit customized and we were getting such error.
Header component where SVG was used and was "causing" the issue.
import Logo from '_public/logos/logo.svg'
import Link from '_components/link/Link'
const Header = () => (
<div className={s.headerLogo}>
<Link href={'/'}>
<Logo />
</Link>
</div>
)
Error Message on Console
Function components cannot be given refs. Attempts to access this ref will fail.
Did you mean to use React.forwardRef()?
Customized Link Component
import NextLink from 'next/link'
import { forwardRef } from 'react'
const Link = ({ href, shallow, replace, children, passHref, className }, ref) => {
return href ? (
<NextLink
href={href}
passHref={passHref}
scroll={false}
shallow={shallow}
replace={replace}
prefetch={false}
className={className}
>
{children}
</NextLink>
) : (
<div className={className}>{children}</div>
)
}
export default forwardRef(Link)
Now we made sure we were using forwardRef in the our customized Link Component but we still got that error.
In order to solve it, I changed the wrapper positioning of SVG element to this and :poof:
const Header = () => (
<Link href={'/'}>
<div className={s.headerLogo}>
<Logo />
</div>
</Link>
)
If you find that you cannot add a custom ref prop or forwardRef to a component, I have a trick to still get a ref object for your functional component.
Suppose you want to add ref to a custom functional component like:
const ref = useRef();
//throws error as Button is a functional component without ref prop
return <Button ref={ref}>Hi</Button>;
You can wrap it in a generic html element and set ref on that.
const ref = useRef();
// This ref works. To get button html element inside div, you can do
const buttonRef = ref.current && ref.current.children[0];
return (
<div ref={ref}>
<Button>Hi</Button>
</div>
);
Of course manage state accordingly and where you want to use the buttonRef object.
to fix this warning you should wrap your custom component with the forwardRef function as mentioned in this blog very nicely
const AppTextField =(props) {return(/*your component*/)}
change the above code to
const AppTextField = forwardRef((props,ref) {return(/*your component*/)}
const renderItem = ({ item, index }) => {
return (
<>
<Item
key={item.Id}
item={item}
index={index}
/>
</>
);
};
Use Fragment to solve React.forwardRef()? warning
If you're using functional components, then React.forwardRef is a really nice feature to know how to use for scenarios like this. If whoever ends up reading this is the more hands on type, I threw together a codesandbox for you to play around with. Sometimes it doesn't load the Styled-Components initially, so you may need to refresh the inline browser when the sandbox loads.
https://codesandbox.io/s/react-forwardref-example-15ql9t?file=/src/App.tsx
// MyAwesomeInput.tsx
import React from "react";
import { TextInput, TextInputProps } from "react-native";
import styled from "styled-components/native";
const Wrapper = styled.View`
width: 100%;
padding-bottom: 10px;
`;
const InputStyled = styled.TextInput`
width: 100%;
height: 50px;
border: 1px solid grey;
text-indent: 5px;
`;
// Created an interface to extend the TextInputProps, allowing access to all of its properties
// from the object that is created from Styled-Components.
//
// I also define the type that the forwarded ref will be.
interface AwesomeInputProps extends TextInputProps {
someProp?: boolean;
ref?: React.Ref<TextInput>;
}
// Created the functional component with the prop type created above.
//
// Notice the end of the line, where you wrap everything in the React.forwardRef().
// This makes it take one more parameter, called ref. I showed what it looks like
// if you are a fan of destructuring.
const MyAwesomeInput: React.FC<AwesomeInputProps> = React.forwardRef( // <-- This wraps the entire component, starting here.
({ someProp, ...props }, ref) => {
return (
<Wrapper>
<InputStyled {...props} ref={ref} />
</Wrapper>
);
}); // <-- And ending down here.
export default MyAwesomeInput;
Then on the calling screen, you'll create your ref variable and pass it into the ref field on the component.
// App.tsx
import React from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";
import MyAwesomeInput from "./Components/MyAwesomeInput";
const App: React.FC = () => {
// Set some state fields for the inputs.
const [field1, setField1] = React.useState("");
const [field2, setField2] = React.useState("");
// Created the ref variable that we'll use down below.
const field2Ref = React.useRef<TextInput>(null);
return (
<View style={styles.app}>
<Text>React.forwardRef Example</Text>
<View>
<MyAwesomeInput
value={field1}
onChangeText={setField1}
placeholder="field 1"
// When you're done typing in this field, and you hit enter or click next on a phone,
// this makes it focus the Ref field.
onSubmitEditing={() => {
field2Ref.current.focus();
}}
/>
<MyAwesomeInput
// Pass the ref variable that's created above to the MyAwesomeInput field of choice.
// Everything should work if you have it setup right.
ref={field2Ref}
value={field2}
onChangeText={setField2}
placeholder="field 2"
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
app: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
export default App;
It's that simple! No matter where you place the MyAwesomeInput component, you'll be able to use a ref.
I just paste here skychavda solution, as it provide a ref to a child : so you can call child method or child ref from parent directly, without any warn.
source: https://github.com/reactjs/reactjs.org/issues/2120
/* Child.jsx */
import React from 'react'
class Child extends React.Component {
componentDidMount() {
const { childRef } = this.props;
childRef(this);
}
componentWillUnmount() {
const { childRef } = this.props;
childRef(undefined);
}
alertMessage() {
window.alert('called from parent component');
}
render() {
return <h1>Hello World!</h1>
}
}
export default Child;
/* Parent.jsx */
import React from 'react';
import Child from './Child';
class Parent extends React.Component {
onClick = () => {
this.child.alertMessage(); // do stuff
}
render() {
return (
<div>
<Child childRef={ref => (this.child = ref)} />
<button onClick={this.onClick}>Child.alertMessage()</button>
</div>
);
}
}