תגובה לקלט עם state

React מספקת דרך הצהרתית לתפעל את ממשק המשתמש. במקום לתפעל חלקים בודדים של ממשק את המשתמש ממש, מתאר את הstate, אתה יכול להיות הרכיב שלך ומחליף בתגובה למשתמש. זה דומה לאופן שבו מעצבים על ממשק המשתמש.

You will learn

  • איך תכנות UI הצהרתי שונה מתכנות UI הכרחי
  • איך למנות את הstates הוויזואליים שיש להם הרכיב שלך יכול להיות
  • איך להפעיל את השינויים בין הstates החזותיים מקוד

איך ממשק משתמש הצהרתי משתווה לציווי

כשאתה מעצב אינטראקציות של ממשק משתמש, אתה כנראה חושב על איך ממשק המשתמש משתנה בתגובה לפעולות המשתמש. שקול טופס המאפשר למשתמש לשלוח תשובה:

  • כשאתה מקליד משהו בטופס, הלחצן “שלח” הופך לזמין.
  • כאשר אתה לוחץ על “שלח”, גם הטופס וגם הכפתור ** הופכים מושבתים,** ומופיע ספינר .
  • אם בקשת הרשת מצליחה, הטופס מוסתר, והודעת “תודה” תופיע.
  • אם בקשת הרשת נכשלת, הודעת שגיאה תופיע והטופס מופעל שוב.

בתכנות חובה, האמור לעיל מתאים ישירות לאופן שבו אתה מיישם אינטראקציה. אתה צריך לכתוב את ההוראות המדויקות כדי לתפעל את ממשק המשתמש בהתאם למה שקרה זה עתה. הנה עוד דרך לחשוב על זה: דמיינו לעצמכם נוסעים ליד מישהו במכונית ואומרים לו סבבה לאן ללכת.

In a car driven by an anxious-looking person representing JavaScript, a passenger orders the driver to execute a sequence of complicated turn by turn navigations.

Illustrated by Rachel Lee Nabors

הם לא יודעים לאן אתה רוצה, הם פשוט פועלים לפי הפקודות שלך. (ואם אתה טועה בהוראות, אתה מגיע למקום הנכון!) זה נקרא חוויתי כי אתה צריך “לפקודה” על כל אלמנט, מהספינר ועד הכפתור, להגיד למחשב איך לעדכן את ה-UI.

בדוגמה זו של תכנות UI חיוני, הטופס בנוי ללא תגובה. הוא משתמש רק בדפדפן DOM:

async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() === 'istanbul') {
        resolve();
      } else {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;

מניפולציה של ממשק המשתמש פועלת באופן הכרחי מספיק טוב עבור דוגמאות בודדות, אך היא הופכת קשה יותר לניהול במערכות מורכבות יותר. דמיינו לעצמכם עדכון של דף מלא בצורות שונות כמו זה. הוספת רכיב ממשק משתמש חדש או אינטראקציה חדשה תדרוש בדיקה קפדנית של כל הקוד הקיים כדי לוודא שלא הצגת באג (לדוגמה, שכחת להציג או להסתיר משהו).

תגיב נבנה כדי לפתור בעיה זו.

ב-React, אינך מבצע מניפולציה ישירה של ממשק משתמש - כלומר מפעיל, משבית, אינך מציג מסתיר רכיבים יחיד. במקום זאת, אתה מצהיר מה אתה רוצה להציג, ו-React מגלה כיצד לעדכן את ממשק המשתמש. תחשוב להיכנס למונית ולהנהג לא אתה רוצה להגיד לו בדיוק לא לפנות. זה התפקיד של הנהג להביא אותך לשם, אולי הם אפילו מכירים קיצורי דרך שלא שקלת!

In a car driven by React, a passenger asks to be taken to a specific place on the map. React figures out how to do that.

Illustrated by Rachel Lee Nabors

חושבים על ממשק משתמש הצהרתי

ראית איך ליישם את האדם האישי למעלה. כדי להבין טוב יותר איך לחשוב ב-React, תעבור על היישום מחדש של ממשק המשתמש הזה ב-React להלן:

  1. זהה את הstates החזותיים של הרכיב שלך
  2. קבע מה גורם לשינויי מצב אלה
  3. להציג את הstate בזיכרון באמצעות ‘useState’
  4. הסר כל משתני מצב שאינם חיוניים
  5. חבר את הרופא המטפלים כדי להגדיר את הstate

שלב 1: זה את הstates החזותיים של הרכיב שלך

במדעי המחשב, אתה שומע על “מכונת state” ישת באחת מכמה “מצבים”. אם אתה עובד עם מעצב, אפשר שראית מוקאפים עבור “מצבים חזותיים” שונים. תגובה עומדת בצומת של מחשב עיצוב ומדעי, כך ששני הרעיונות הללו הם מקורות השראה.

ראשית, עליך לדמיין את כל ה”מצבים” השונים של ממשק המשתמש שהמשתמש עשוי לראות:

  • ריק: בטופס יש כפתור “שלח” מושבת.
  • הקלדה: לטופס יש כפתור “שלח” מופעל.
  • שליחה: הטופס מושבת לחלוטין. ספינר מוצג.
  • הצלחה: הודעת “תודה” מוצגת במקום טופס.
  • שגיאה: זהה לstate ההקלדה, אבל עם הודעת שגיאה נוספת.

בדיוק כמו מעצב, תרצה “לעגוג” או ליצור “לעג” עבור הstates לפני שתוסיף את היגיון. לדוגמה, הנה לעג רק לחלק את הוויזואל של הטופס. הדמה הזו נשלטת על ידי props שנקרא סטטוס עם ערך ברירת המחדל של 'ריק':

export default function Form({
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Submit
        </button>
      </form>
    </>
  )
}

אתה יכול לקרוא לprops הזה כל דבר שתרצה, השם לא חשוב. נסה לערוך את status = 'ריק' ל-status = 'הצלחה' כדי לראות את הודעת ההצלחה מופיעה. אתה יכול ללכת מהר על ממשק המשתמש לפני מחבר היגיון. הנה אב טיפוס יותר מושכל של אותו רכיב, שעדיין “נשלט” על ידי props ה’סטטוס’:

export default function Form({
  // Try 'submitting', 'error', 'success':
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea disabled={
          status === 'submitting'
        } />
        <br />
        <button disabled={
          status === 'empty' ||
          status === 'submitting'
        }>
          Submit
        </button>
        {status === 'error' &&
          <p className="Error">
            Good guess but a wrong answer. Try again!
          </p>
        }
      </form>
      </>
  );
}

Deep Dive

הצגת מצבים חזותיים רבים בו-זמנית

אם לרכיב יש הרבה מצבים חזותיים, זה יכול להיות נוח להציג את כולם בעמוד אחד:

import Form from './Form.js';

let statuses = [
  'empty',
  'typing',
  'submitting',
  'success',
  'error',
];

export default function App() {
  return (
    <>
      {statuses.map(status => (
        <section key={status}>
          <h4>Form ({status}):</h4>
          <Form status={status} />
        </section>
      ))}
    </>
  );
}

דפים כמו זה נקראים לעתים קרובות “מדריכי סגנון חיים” או “ספרי סיפורים”.

שלב 2: קבע מה גורם לשינוי הstate האלה

אתה יכול להפעיל עדכוני מצב בתגובה לשני סוגים של קלט:

  • תשומות אנושיות, כמו לחיצה על כפתור, הקלדה בשדה, ניווט בקישור.
  • כניסות מחשב, כמו תגובת רשת שמגיעה, השלמת פסק זמן, טעינת תמונה.
A finger.
Human inputs
Ones and zeroes.
Computer inputs

Illustrated by Rachel Lee Nabors

בשני המקרים, עליך להגדיר משתני מצב כדי לעדכן את המשתמש ממש. עבור הטופס שאתה מפתח, לשנות את הstate בתגובה: לכמה כניסות.

  • שינוי קלט הטקסט (אנושי) אמור להעביר אותו מstate ריק לstate הקלדה או חזרה, תלוי אם תיבת הטקסט ריקה או לא.
  • לחיצה על כפתור שלח (אנושית) אמורה להעביר אותו לstate מגיש.
  • תגובת רשת מוצלחת (מחשב) אמורה להעביר אותה לstate הצלחה.
  • תגובת רשת נכשלה (מחשב) אמורה להעביר אותה לstate שגיאה עם הודעת השגיאה התואמת.

Note

שימו לב שקלט אנושי דורש טיפול [מטפלי אירועים](/למד/מגיבים לאירועים)!

כדי לעזור לדמיין את הזרימה הזו, נסה לצייר כל מצב על נייר כעיגול מסומן, וכל שינוי בין שני מצבים כחץ. אתה יכול לשרטט זרימות רבות בדרך זו ולסדר באגים הרבה לפני היישום.

Flow chart moving left to right with 5 nodes. The first node labeled 'empty' has one edge labeled 'start typing' connected to a node labeled 'typing'. That node has one edge labeled 'press submit' connected to a node labeled 'submitting', which has two edges. The left edge is labeled 'network error' connecting to a node labeled 'error'. The right edge is labeled 'network success' connecting to a node labeled 'success'.
Flow chart moving left to right with 5 nodes. The first node labeled 'empty' has one edge labeled 'start typing' connected to a node labeled 'typing'. That node has one edge labeled 'press submit' connected to a node labeled 'submitting', which has two edges. The left edge is labeled 'network error' connecting to a node labeled 'error'. The right edge is labeled 'network success' connecting to a node labeled 'success'.

מצבי טופס

שלב 3: להציג את הstate בזיכרון באמצעות useState

בשלב הבא תעבוד להצגת הstates הוויזואליים של הרכיב שלך בזיכרון עם useState. הפשטות היא הפתח: כל חלק של מצב הוא “חלק נע”, ואתה רוצה כמה שפחות “חלקים זזים” ככל האפשר. יותר מורכבות מובילים ליותר באגים!

תתחיל מstate שבהחלט חייבת להיות שם. לדוגמה, תצטרך לאחסן את ה’תשובה’ עבור הקלט, ואת ה’שגיאה’ (אם היא קיימת) כדי לאחסן את השגיאה האחרונה:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

לאחר מכן, תזדקק לשינוי מצב המציג את אחד מstates החזותיים שברצונך להציג. בדרך כלל יש יותר מדרך אחת להצגת זה בזיכרון, אז תרצו להתנסות עם זה.

אם אתה מתקשה לחשוב על הדרך הטובה ביותר, התחל בהוספת מספיק מצבים שאתה בהחלט בטוח שכל הstates החזותיים האפשריים מכוסים:

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

סביר להניח שהרעיון הראשון שלך לא יהיה הטוב ביותר, אבל זה בסדר - מצב החזרה הוא חלק מהתהליך!

שלב 4: הסר משתני מצב חיוניים

אתה רוצה למנוע כפילות בתוכן הstate אז אתה עוקב רק אחר מה שחשוב. השקעת זמן קצרה בשינוי מבנה הstate שלך יקל על ההבנה של הרכיבים שלך, יאפשר כפילות ויימנע ממשמעויות לא מכוונות. המטרה היא שלך למנוע את המקרים הstate בזיכרון אינו מייצג שום ממשק משתמש חוקי ( רוצה שמשתמש יראה. לדוגמא, לעולם לא תרצה להציג הודעת שגיאה ולהשבית את הקלט בו-זמנית, או שהמשתמש לא יוכל לתקן את השגיאה!)

הנה כמה שאלות שאתה יכול לשאול לגבי משתני הstate שלך:

  • האם מצב זה גורם לפרדוקס? לדוגמה, isTyping ו-issubmitting לא יכולים להיות שניהם נכונים. פרדוקס פירושו בדרך כלל שstate אינה מוגבלת מספיק. יש ארבעה שילובים אפשריים של שני בוליאנים, אך רק שלושה תואמים מצבים תקפים. להוציא את הstate ה”כדי בלתי אפשרי”, אתה יכול לשלב אותם ל’סטטוס’ שחייב להיות ערכים משלו אחדים: ”הקלדה’, ''שליחת' או ”הצלחה’`.
  • האם אותו מידע זמין כבר בשינוי מצב אחר? פרדוקס נוסף: isEmpty ו-isTyping לא יכולים להיות true בו-זמנית. על ידי הפיכתם למשתני מצב נפרדים, אתה מסתכן שהם יצאו מסנכרון ויגרמו לבאגים. למרבה המזל, אתה יכול להוציא את isEmpty_= __TK_8th.
  • האם אתה יכול לקבל אותו מידע מהיפוך של שינוי מצב אחר? אין צורך ב-‘isError’ כי אתה יכול לבדוק את ‘error !== null’ במקום זאת.

לאחר הניקוי הזה, נשארו לך 3 (ירידה מ-7!) חיוניים משתני מצב:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

אתה יודע שהם חיוניים, כי אתה לא יכול להסיר אף אחד מהם מבלי לשבור את הפונקציונליות.

Deep Dive

ביטול מצבים “בלתי אפשריים” עם מפחית

שלושת המשתנים הללו הם ייצוג מספיק טוב של מצב הצורה הזו. עם זאת, יש לי ממש כמה מצבי. לדוגמה, שגיאה אינה אפס אינה הגיונית כאשר סטטוס הוא הצלחה. כדי לדגמן את הstate בצורה מדויקת יותר, אתה לחלץ אותו לתוך המפחית. reducer לך לאחד שני מרובים למצב אחד ולאחד את כל ההיגיון הקשור!

שלב 5: חברו את המטפלים לעדכון מצב TK_0__

לבסוף, צורפי אירועים שמעדכנים את הstate. להלן הטופס הסופי, עם כל מטפלים מחוברים:

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

למרות שהקוד הזה ארוך יותר מהדוגמה המקורית של הציווי, הוא הרבה פחות שביר. ביטוי כל האינטראקציות כשינויי מצב יכול להראות מאוחר יותר. זה גם יכול לשנות את מה שצריך להיות מוצג בכל מצב את ההיגיון של האינטראקציה עצמה.

Recap

  • תכנות הצהרתי פירושו תיאור ממשק המשתמש עבור כל מצב ויזואלי במקום ניהול מיקרו של ממשק המשתמש (הכרח).
  • בעת פיתוח רכיב:
  1. זהה את כל הstates החזותיים שלו.
  2. קבע את הטריגרים האנושיים והמחשבים לשינויי מצב.
  3. דגם את הstate באמצעות ‘useState’.
  4. הסר מצב לא חיוני כדי למנוע באגים ופרדוקסים.
  5. חבר את המטפלים לstate מוגדר.

Challenge 1 of 3:
הוסף והסר מחלקת CSS

הפוך את זה כך שלחיצה על התמונה תסיר את המחלקה ה-CSS background--active מהמחלקה החיצונית <div>, אך מוסיפה את המחלקה picture--active למחלקה <img>. לחיצה נוספת על הרקע אמורה לשחזר את מחלקות ה-CSS המקוריות.

מבחינה ויזואלית, אתה צריך לצפות שלחיצה על התמונה תסיר את הרקע הסגול ותדגיש את גבול התמונה. לחיצה מחוץ לתמונה מדגישה את הרקע, אך מסירה את סימון גבול התמונה.

export default function Picture() {
  return (
    <div className="background background--active">
      <img
        className="picture"
        alt="Rainbow houses in Kampung Pelangi, Indonesia"
        src="https://i.imgur.com/5qwVYb1.jpeg"
      />
    </div>
  );
}