

### 解決したいこと

railsのパラメータのエラーを直したいです。

### 発生している問題・エラー

[code]

    ActionController::ParameterMissing: param is missing or the value is empty: learning_material
    from /usr/local/bundle/gems/actionpack-7.0.3.1/lib/action_controller/metal/strong_parameters.rb:495:in `require'
    
[/code]

### 環境と流れ

・Dockerを使い、バックエンド側（Rails）とフロントエンド側(TypeScript
React)に分けて開発をしています。OSはWindowsですが、WSL2のLinux（Ubuntu20.04）を使用しています。  
・React側のフォームで入力データを受け取り、それをRails側に保存する。

### backend側（＊長いため関係がありそうなコードのみ）

### /backend/app/controllers/api/v1/learning_materials_controller.rb

[code]

    
    class Api::V1::LearningMaterialsController < ApplicationController
      def create
        user = User.find(params[:user_id])
        binding.pry
        learning_material = user.learning_materials.create!(learning_material_params)
        
        if learning_material.save
          render json: learning_material
        else
          render json: learning_material.errors, status: 422
        end
      end
    
    
      private
        def learning_material_params
          params.require(:learning_material).permit(:subject, :body, :file, :answer)
        end
    end
    
    
[/code]

### user.rb

[code]

    class User < ApplicationRecord
                # Include default devise modules.
                devise :database_authenticatable, :registerable,
                        :recoverable, :rememberable, 
                        # :trackable, 
                        :validatable,
                        # :confirmable,
                        :omniauthable
                include DeviseTokenAuth::Concerns::User
        has_many :learning_materials, dependent: :destroy
    end
    
[/code]

### learning_material.rb

[code]

    class LearningMaterial < ApplicationRecord
      belongs_to :user
      mount_uploader :file, FileUploader
        default_scope -> { order(created_at: :desc) }
        validates :subject,presence:true
        validates :body,   presence:true
        has_one_attached :file
        validates :user_id, presence: true
    end
    
[/code]

### routes.rb

[code]

    Rails.application.routes.draw do
      mount_devise_token_auth_for 'User', at: 'auth'
      namespace :api do
        namespace :v1 do
          
          mount_devise_token_auth_for 'User', skip: [:omniauth_callbacks], at: 'auth', controllers: {
            registrations: 'api/v1/auth/registrations'
          }
    
          namespace :auth do
            resources :sessions, only: %i[index]
          end
          resources :users do
            resources :learning_materials
          end
        end
      end
    end
    
    
[/code]

### front側

### App.tsx

[code]

    import React, { useState, useEffect, createContext } from "react"
    import { BrowserRouter, Routes, Route, useParams, Link, Navigate } from "react-router-dom";
    
    import CommonLayout from "./templates/CommonLayout"
    import { getCurrentUser } from "apis/auth"
    import { User } from "interfaces/index"
    import  LearningMaterialList  from "./components/organisms/LearningMaterialList";
    
    // グローバルで扱う変数・関数
    export const AuthContext = createContext({} as {
      loading: boolean
      setLoading: React.Dispatch<React.SetStateAction<boolean>>
      isSignedIn: boolean
      setIsSignedIn: React.Dispatch<React.SetStateAction<boolean>>
      currentUser: User | undefined
      setCurrentUser: React.Dispatch<React.SetStateAction<User | undefined>>
    })
    
    
    const App: React.FC = () => {
      const { userId } = useParams();
    
    
      return (
    
      <>
        <BrowserRouter>
          <AuthContext.Provider value={{ loading, setLoading, isSignedIn, setIsSignedIn, currentUser, setCurrentUser}}>
            <CommonLayout>
              <Routes>
    
                <Route path="users/:usersId" element={<LearningMaterialList />} />
                <Route path="users/:usersId" element={<Users />} />
                <Route
                          path="/"
                          element={
                          <PrivateRoute>
                            <Home />
                          </PrivateRoute>
                          }
                        />
              </Routes>
            </CommonLayout>
          </AuthContext.Provider>
        </BrowserRouter>
      </>
      );
    }
    
    export default App;
    
[/code]

### LearningMaterialList.tsx

[code]

    import React, { useEffect, useState } from "react"
    import { useParams } from "react-router-dom";
    import { Container, Grid } from "@material-ui/core"
    // import { makeStyles } from "@material-ui/core/styles"
    import { makeStyles, createStyles } from '@mui/styles';
    
    import LearningMaterialForm from "./LearningMaterialForm"
    import LearningMaterialItem from "./LearningMaterialItem"
    
    import { getLearningMaterials } from "../../apis/learning_material"
    import { LearningMaterial } from "../../interfaces/index"
    
    const useStyles = makeStyles(() => ({
      container: {
        marginTop: "3rem"
      }
    }))
    
    const PostList: React.FC = () => {
      const classes = useStyles()
      const [learning_materials, setLearningMaterials] = useState<LearningMaterial[]>([])
      const { usersId } = useParams();
      const handleGetLMs = async () => {
        
        const { data }  = await getLearningMaterials(usersId)
        setLearningMaterials(data.learning_materials)
      }
    
      useEffect(() => {
        handleGetLMs()
      }, [])
    
      return (
        <Container maxWidth="lg" className={classes.container}>
          <Grid container direction="row" justifyContent="center">
            <Grid item>
              <LearningMaterialForm
                handleGetPosts={handleGetLMs}
              />
              { learning_materials?.map((learning_material: LearningMaterial) => {
                return (
                  <LearningMaterialItem
                    key={learning_material.id}
                    learning_material={learning_material}
                    handleGetLMs={handleGetLMs}
                  />
                )}
              )}
              <h1>LearningMaterial</h1>
            </Grid>   
          </Grid>
        </Container>
      )
    }
    
    export default PostList
    
[/code]

### LearningMaterialForm.tsx

[code]

    import React, { useCallback, useState } from "react"
    import { useParams } from "react-router-dom";
    import { experimentalStyled as styled } from '@material-ui/core/styles';
    
    import { Theme } from "@material-ui/core/styles"
    import TextField from "@material-ui/core/TextField"
    import Button from "@material-ui/core/Button"
    import Box from "@material-ui/core/Box"
    import IconButton from "@material-ui/core/IconButton"
    import PhotoCameraIcon from "@material-ui/icons/PhotoCamera"
    import CancelIcon from "@material-ui/icons/Cancel"
    
    import { makeStyles, createStyles } from '@mui/styles';
    
    import { createLearningMaterials } from "../../apis/learning_material"
    // frontend/app/src/apis/learning_material.ts
    const useStyles = makeStyles((theme: Theme) => ({
      form: {
        display: "flex",
        flexWrap: "wrap",
        width: 320
      },
      inputFileBtn: {
        marginTop: "10px"
      },
      submitBtn: {
        marginTop: "10px",
        marginLeft: "auto"
      },
      box: {
        margin: "2rem 0 4rem",
        width: 320
      },
      preview: {
        width: "100%"
      }
    }))
    
    const Input = styled("input")({
      display: "none"
    })
    
    const borderStyles = {
      bgcolor: "background.paper",
      border: 1,
    }
    
    interface PostFormProps {
      handleGetPosts: Function
    }
    
    const PostForm = ({ handleGetPosts }: PostFormProps) => {
      const classes = useStyles()
      const { usersId } = useParams();
    
      const [subject, setSubject] = useState<string>("")
      const [body, setBody] = useState<string>("")
      const [answer, setAnswer] = useState<string>("")
      const [file, setFile] = useState<File>()
      const [preview, setPreview] = useState<string>("")
    
      const uploadImage = useCallback((e: any) => {
        const file = e.target.files[0]
        setFile(file)
      }, [])
    
      // プレビュー機能
      const previewImage = useCallback((e: any) => {
        const file = e.target.files[0]
        setPreview(window.URL.createObjectURL(file))
      }, [])
    
      // FormData形式でデータを作成
      const createFormData = (): FormData => {
        const formData = new FormData()
    
        formData.append("subject", subject)
        formData.append("body", body)
        if (file) formData.append("file", file)
        formData.append("answer", answer)
        
    
        return formData
      }
    
      const handleCreatePost  = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()
    
        const data = createFormData()
    
        await createLearningMaterials(usersId,data)
        .then(() => {
          setSubject("")
          setBody("")
          setAnswer("")
          setPreview("")
          setFile(undefined)
          handleGetPosts()
        })
      }
    
      return (
        <>
          <form className={classes.form} noValidate onSubmit={handleCreatePost}>
            <TextField
              placeholder="Hello World"
              variant="outlined"
              multiline
              fullWidth
              rows="4"
              value={subject}
              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                setSubject(e.target.value)
              }}
            />
            <TextField
              placeholder="Hello World"
              variant="outlined"
              multiline
              fullWidth
              rows="4"
              value={body}
              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                setBody(e.target.value)
              }}
            />
            <TextField
              placeholder="Hello World"
              variant="outlined"
              multiline
              fullWidth
              rows="4"
              value={answer}
              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                setAnswer(e.target.value)
              }}
            />
            <div className={classes.inputFileBtn}>
              <label htmlFor="icon-button-file">
                <Input
                  // accept="image/*"
                  id="icon-button-file" 
                  type="file"
                  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                    uploadImage(e)
                    previewImage(e)
                  }}
                />
                <IconButton color="inherit" component="span">
                  <PhotoCameraIcon />
                </IconButton>
              </label>
            </div>
            
            <div className={classes.submitBtn}>
              <Button
                type="submit"
                variant="contained"
                size="large"
                color="inherit"
                disabled={!subject || subject.length > 140 || !body || body.length > 140}
                className={classes.submitBtn}
              >
                Post
              </Button>
            </div>
          </form>
          { preview ?
            <Box
              sx={{ ...borderStyles, borderRadius: 1, borderColor: "grey.400" }}
              className={classes.box}
            >
              <IconButton
                color="inherit"
                onClick={() => setPreview("")}
              >
                <CancelIcon />
              </IconButton>
              <img
                src={preview}
                alt="preview img"
                className={classes.preview}
              />
            </Box> : null
          }
        </>
      )
    }
    
    export default PostForm
    
[/code]

### apis/learning_material.ts

[code]

    import { AxiosPromise } from "axios"
    import client_lm from "./client_lm"
    import { LearningMaterialApiJson } from "../interfaces/index"
    
    // 取得
    export const getLearningMaterials = (usersId: string|undefined): AxiosPromise<LearningMaterialApiJson> => {
      return client_lm.get(`${usersId}/learning_materials`)
    }
    
    // 作成
    export const createLearningMaterials = (usersId: string|undefined, data: FormData): AxiosPromise => {
      return client_lm.post(`${usersId}/learning_materials`, data)
    }
    
    // 削除
    export const deleteLearningMaterials = (usersId: string|undefined, id: string): AxiosPromise => {
      return client_lm.delete(`${usersId}/learning_materials/${id}`)
    }
    
[/code]

### 自分で試したこと

binding.pryを利用して以下のことを確認しました。  
・userはきちんと値が取れている  
・learning_material_paramsは以下のエラーを吐く

[code]

    ActionController::ParameterMissing: param is missing or the value is empty: learning_material
    from /usr/local/bundle/gems/actionpack-7.0.3.1/lib/action_controller/metal/strong_parameters.rb:495:in `require'
    
[/code]

・learning_materialはnil  
・この場面（binding.pryを利用している場面）でparamsを調べると

[code]

    #<ActionController::Parameters {"subject"=>"f", "body"=>"f", "file"=>#<ActionDispatch::Http::UploadedFile:0x00007fe40d5f9a30 @tempfile=#<Tempfile:/tmp/RackMultipart20220802-1-ixp5lc.mp3>, 
    @original_filename="Night_Sea.mp3", @content_type="audio/mpeg", @headers="Content-Disposition: form-data; name=\"file\"; filename=\"Night_Sea.mp3\"\r\nContent-Type: audio/mpeg\r\n">, 
    "answer"=>"f", "controller"=>"api/v1/learning_materials", "action"=>"create", "user_id"=>"3"} permitted: false>
    
[/code]

なぜかこのように正常にパラメータを取れている。  
・エラーになっているファイルを開くと

[code]

    [7] pry(#<Api::V1::LearningMaterialsController>)> /usr/local/bundle/gems/actionpack-7.0.3.1/lib/action_controller/metal/strong_parameters.rb
    (eval):2: unknown regexp options - lcal
    api_1    | /usr/local/bundle/gems/actionpack-7.0.3...
    api_1    |     ^~~~~~
    api_1    | (eval):2: unexpected fraction part after numeric literal
    pi_1    | .../bundle/gems/actionpack-7.0.3.1/lib/action_controller/metal/...
    api_1    | ...                           ^~~~
    
[/code]

このようになっているが、なぜこのようなエラーが起きるのか分からない。

・以上のことから、ストロングパラメータを利用している部分に何かしら問題があると予想しました。しかし、コントローラーファイルのどこが間違っているのか分かりません。  
公開しているファイルに不備があれば追加で投稿します。何かしらアドバイスがあればよろしくお願いいたします。


CC BY-SA 4.0
2022-08-02T08:19:37.520
90334
2022-08-02T10:11:32.033
51193
question
ruby-on-rails docker reactjs typescript
ParameterMissingエラーを解決したい


解決致しました。どうやらストロングパラメータの記述方法が間違っていたようです。正しくは以下の通りです。

[code]

    private
        def learning_material_params
          # params.require(:learning_material).permit(:subject, :body, :file, :answer) ###ダメな例
          params.permit(:subject, :body, :file, :answer) ###正しい例
        end
    
[/code]


CC BY-SA 4.0
2022-08-02T10:11:32.033
90337
2022-08-02T10:11:32.033
51193
90334
answer