-
Add a
Header
component in the components folder, which renders anh1
element -
Header
should accept atext
prop and render it -
Style the
Header
with thestyles.heading
-
Add
Header
to theApp
instead of the existingh1
element, with Character Creator as the text
-
Add a
Button
component in the components folder, which renders abutton
element -
Button
should accept atext
prop and render it -
Style the
Button
with thestyles.button
-
Add
Button
to theApp
as a child of thediv.creatorInputs
element, with Generate as the text -
Add an
onClick
prop to theButton
-
Use
console.log
to log Generate button clicked! in theApp
when the button is clicked
-
Add an
Input
component in the components folder, which renders aninput
element -
Input
should accept avalue
andonChange
props. Bind the props to the input'svalue
andonChange
properties. Use this code to send the new value of the input to theonChange
handler:onChange={(e) => props.onChange(e.target.value)}
-
Add a new
Input
to theApp
as a childdiv.creatorInputs
container, just before the button -
Hook into
useState
with an empty string as the default value, and place the state getter and setter variables as props toInput
. The state variables should be namedname
andsetName
. -
Modify the
Button.onClick
so that it logs the state value from theInput
-
In the
Input
component, add adiv
container withstyles.inputBox
and place theinput
as a child of this container -
Before
input
, add alabel
element. The element should render alabel
prop -
In the
App
, add a label to the input component. The label should have the value Name. -
Underneath the first add another
Input
component. This component should hook into its own state and have a different label. The state variables should be namedsuperpower
andsetSuperpower
. The label should have the value Superpower. -
Modify the
Button.onClick
so that it logs state from both inputs.
-
Add a
Radio
component in the components folder -
This component should accept an
options
prop and dynamically renderinput
elements withtype='radio'
for each option
props.options.map
array function should be used for this- each input should also have:
id
property bound tooption
name
property bound tooption
checked
property bound to a boolean value that is the result ofprops.value === option
onChange
property that calls theprops.onChange
with the currentoption
- The whole component should have the following structure:
<div className={styles.inputBox}> <label>{props.label}</label> <div className={styles.radioGroup}> {props.options.map((option) => ( <div key={option} className={styles.radioBox}> <label htmlFor={option}>{option}</label> // ...option input </div> ))} </div> </div>
- more info on the appropriate structure of a radio input can be found on this link.
-
In the
App
add a newRadio
component after the lastInput
-
Hook into
useState
with'female'
as the default value, and place the state getter and setter variables as props toRadio
. The state variables should be namedgender
andsetGender
. -
Add also an
options={["male", "female"]}
prop, and anlabel="Gender"
prop -
Modify the
Button.onClick
so that it logs the new state also.
- Add a
Textarea
component in the components folder, which renders atextarea
element. The component should have the following structure:
<div className={styles.inputBox}>
// ... label
// ... textarea
</div>
-
The
label
element should render thelabel
prop -
Textarea
should also accept avalue
andonChange
props. Bind the props to the textarea'svalue
andonChange
properties. -
Add also the
cols
androws
properties to thetextarea
element with values30
and5
respectively -
In the
App
component add a newTextarea
component after theRadio
-
Hook into
useState
with an empty string as the default value, and place the state getter and setter variables as props toTextarea
. The state variables should be nameddescription
andsetDescription
-
Bind the
label
prop onTextarea
to the value Description -
Modify the
Button.onClick
so that it logs the new state also
-
In
App
add aSeparator
andImage
components afterdiv.creatorInputs
(these components are already present in the components folder) -
Hook into
useGeneratedImage
(import from localhooks
file) and spread its variables:getImage
,imageUrl
,isIdle
,isLoading
,isError
,isSuccess
getImageUrl
is a function that sends a prompt to the image generation service- other variables contain request status flags and the generated image url
- Modify the
Button.onClick
handler. When the button is clicked a valid string prompt should be made and sent to the image generation service
- create a new function for that in the
App
body calledhandleGenerate
- you can generate the prompt with the
generatePromptFromCharacter
util (import from localutils
file) - the generated prompt should be sent to the image generation service with the
getImageUrl
function
- Bind
Image
props to the rest of the variables fromuseGeneratedImage
Currently, we are keeping the character
data in 4 different state variables (one for each input: name, superpower,
gender, description). We can combine these in a single state object. Each input component will then be responsible for
rendering and changing a specific slice of the combines state.
Combining related state in a single object is a common technique, which is recommended when the state is related. You can read about the details in this link.
-
Delete the 4
useState
hooks in theApp
component. Take note of the default values for each state - it will be used in the next step -
Hook into
useState
and use an object as a default value. The object should have the following properties:name
,superpower
,gender
,description
, with the values corresponding to default values inuseState
hooks that were deleted in the previous step. -
Modify each character input component so that it renders the appropriate character property. It should also modify only that property in its
onChange
handler. For example:
<Input label="Name" value={character.name} onChange={(name) => setCharacter({ ...character, name })} />
All of the components that are used in the App
are defined in the components
folder. When we import these components
we have to target their files. This results a large number of import statements that can be seen at the top of the
App.js
:
import { Header } from "./components/Header";
import { Button } from "./components/Button";
import { Input } from "./components/Input";
import { Radio } from "./components/Radio";
import { Textarea } from "./components/Textarea";
import { Separator } from "./components/Separator";
import { Image } from "./components/Image";
This is not the pretties thing to look at and, as the project grows, can affect readability of our code. This problem
can be elegantly solved by re-exporting each component from a combined index.js
file. This technique is called a
barrel export, and results in a single import statement for all the related components:
import { Header, Button, Input, Radio, Textarea, Separator, Image } from "./components";
You can read a quick summary on how to achieve this on this link. Or try googling something like react barrel exports.
Setup barrel exports in the components folder, and import all the components in the App
with a single statement.
Our App
component is functional and we have the behaviour that we want nailed down. That's great!
On the other hand we can still make improvements to our code, which can make it more readable and maintainable in the long run. Changes like these, which improve the overall structure of the code but do not impact the general logic and functionalities are called refactors.
A common refactor in React involves extracting components. When we see a number of components that have some kind of shared UI and behaviour it often advisable to extract them in a single component. This makes the code more understandable and easier to maintain.
A great candidate for that is the div.creatorInputs
container with its child inputs and the Generate button. These
inputs are pieces of the same UI (they represent a character creator form) and they manipulate the same state. Create a
separate component that will contain these component and their state. The component will have a single prop onGenerate
where a generated character prompt will be sent (use the generatePromptFromCharacter
for that). We can bind to this
prop in the parent App
with the function getImageUrl
. The component will be used like this:
<CharacterForm onGenerate={getImageUrl} />
You can define the component underneath the App
in the same file.